From a69ae6b89f0560b2dfc97e86eebd1268e945e2b9 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 11:36:52 +0200 Subject: [PATCH 01/45] [CCR] Add metadata to keep track of the index uuid of the leader index in the follow index (#33367) The follow index api checks if the recorded uuid in the follow index matches with uuid of the leader index and fails otherwise. This validation will prevent a follow index from following an incompatible leader index. The create_and_follow api will automatically add this custom index metadata when it creates the follow index. Closes #31505 --- .../java/org/elasticsearch/xpack/ccr/Ccr.java | 1 + .../TransportCreateAndFollowIndexAction.java | 1 + .../action/TransportFollowIndexAction.java | 8 ++ .../xpack/ccr/ShardChangesIT.java | 83 ++++++++++--------- .../TransportFollowIndexActionTests.java | 55 ++++++++---- 5 files changed, 92 insertions(+), 56 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java index 4e4caf8500f..eddb3570dee 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/Ccr.java @@ -87,6 +87,7 @@ public class Ccr extends Plugin implements ActionPlugin, PersistentTaskPlugin, E public static final String CCR_THREAD_POOL_NAME = "ccr"; public static final String CCR_CUSTOM_METADATA_KEY = "ccr"; public static final String CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS = "leader_index_shard_history_uuids"; + public static final String CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY = "leader_index_uuid"; private final boolean enabled; private final Settings settings; diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java index c6d1a7c36c5..e795a903729 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportCreateAndFollowIndexAction.java @@ -183,6 +183,7 @@ public final class TransportCreateAndFollowIndexAction // Adding the leader index uuid for each shard as custom metadata: Map metadata = new HashMap<>(); metadata.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, String.join(",", historyUUIDs)); + metadata.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, leaderIndexMetaData.getIndexUUID()); imdBuilder.putCustom(Ccr.CCR_CUSTOM_METADATA_KEY, metadata); // Copy all settings, but overwrite a few settings. diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java index fff3f1618aa..9e1d2cc44ac 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexAction.java @@ -245,6 +245,14 @@ public class TransportFollowIndexAction extends HandledTransportAction { @@ -339,7 +335,7 @@ public class ShardChangesIT extends ESIntegTestCase { final FollowIndexAction.Request followRequest = new FollowIndexAction.Request("index1", "index2", randomIntBetween(32, 2048), randomIntBetween(2, 10), Long.MAX_VALUE, randomIntBetween(2, 10), FollowIndexAction.DEFAULT_MAX_WRITE_BUFFER_SIZE, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); - client().execute(FollowIndexAction.INSTANCE, followRequest).get(); + client().execute(CreateAndFollowIndexAction.INSTANCE, new CreateAndFollowIndexAction.Request(followRequest)).get(); long maxNumDocsReplicated = Math.min(1000, randomLongBetween(followRequest.getMaxBatchOperationCount(), followRequest.getMaxBatchOperationCount() * 10)); @@ -416,34 +412,6 @@ public class ShardChangesIT extends ESIntegTestCase { expectThrows(IndexNotFoundException.class, () -> client().execute(FollowIndexAction.INSTANCE, followRequest3).actionGet()); } - @TestLogging("_root:DEBUG") - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33379") - public void testValidateFollowingIndexSettings() throws Exception { - assertAcked(client().admin().indices().prepareCreate("test-leader") - .setSettings(Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), true))); - // TODO: indexing should be optional but the current mapping logic requires for now. - client().prepareIndex("test-leader", "doc", "id").setSource("{\"f\": \"v\"}", XContentType.JSON).get(); - assertAcked(client().admin().indices().prepareCreate("test-follower").get()); - IllegalArgumentException followError = expectThrows(IllegalArgumentException.class, () -> client().execute( - FollowIndexAction.INSTANCE, createFollowRequest("test-leader", "test-follower")).actionGet()); - assertThat(followError.getMessage(), equalTo("the following index [test-follower] is not ready to follow;" + - " the setting [index.xpack.ccr.following_index] must be enabled.")); - // updating the `following_index` with an open index must not be allowed. - IllegalArgumentException updateError = expectThrows(IllegalArgumentException.class, () -> { - client().admin().indices().prepareUpdateSettings("test-follower") - .setSettings(Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true)).get(); - }); - assertThat(updateError.getMessage(), containsString("Can't update non dynamic settings " + - "[[index.xpack.ccr.following_index]] for open indices [[test-follower/")); - assertAcked(client().admin().indices().prepareClose("test-follower")); - assertAcked(client().admin().indices().prepareUpdateSettings("test-follower") - .setSettings(Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true))); - assertAcked(client().admin().indices().prepareOpen("test-follower")); - assertAcked(client().execute(FollowIndexAction.INSTANCE, - createFollowRequest("test-leader", "test-follower")).actionGet()); - unfollowIndex("test-follower"); - } - public void testFollowIndex_lowMaxTranslogBytes() throws Exception { final String leaderIndexSettings = getIndexSettings(1, between(0, 1), singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); @@ -478,6 +446,37 @@ public class ShardChangesIT extends ESIntegTestCase { unfollowIndex("index2"); } + public void testDontFollowTheWrongIndex() throws Exception { + String leaderIndexSettings = getIndexSettings(1, 0, + Collections.singletonMap(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true")); + assertAcked(client().admin().indices().prepareCreate("index1").setSource(leaderIndexSettings, XContentType.JSON)); + ensureGreen("index1"); + assertAcked(client().admin().indices().prepareCreate("index3").setSource(leaderIndexSettings, XContentType.JSON)); + ensureGreen("index3"); + + FollowIndexAction.Request followRequest = new FollowIndexAction.Request("index1", "index2", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + CreateAndFollowIndexAction.Request createAndFollowRequest = new CreateAndFollowIndexAction.Request(followRequest); + client().execute(CreateAndFollowIndexAction.INSTANCE, createAndFollowRequest).get(); + + followRequest = new FollowIndexAction.Request("index3", "index4", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + createAndFollowRequest = new CreateAndFollowIndexAction.Request(followRequest); + client().execute(CreateAndFollowIndexAction.INSTANCE, createAndFollowRequest).get(); + unfollowIndex("index2", "index4"); + + FollowIndexAction.Request wrongRequest1 = new FollowIndexAction.Request("index1", "index4", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + Exception e = expectThrows(IllegalArgumentException.class, + () -> client().execute(FollowIndexAction.INSTANCE, wrongRequest1).actionGet()); + assertThat(e.getMessage(), containsString("follow index [index4] should reference")); + + FollowIndexAction.Request wrongRequest2 = new FollowIndexAction.Request("index3", "index2", 1024, 1, 1024L, + 1, 10240, TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(10)); + e = expectThrows(IllegalArgumentException.class, () -> client().execute(FollowIndexAction.INSTANCE, wrongRequest2).actionGet()); + assertThat(e.getMessage(), containsString("follow index [index2] should reference")); + } + private CheckedRunnable assertTask(final int numberOfPrimaryShards, final Map numDocsPerShard) { return () -> { final ClusterState clusterState = client().admin().cluster().prepareState().get().getState(); @@ -514,10 +513,12 @@ public class ShardChangesIT extends ESIntegTestCase { }; } - private void unfollowIndex(String index) throws Exception { - final UnfollowIndexAction.Request unfollowRequest = new UnfollowIndexAction.Request(); - unfollowRequest.setFollowIndex(index); - client().execute(UnfollowIndexAction.INSTANCE, unfollowRequest).get(); + private void unfollowIndex(String... indices) throws Exception { + for (String index : indices) { + final UnfollowIndexAction.Request unfollowRequest = new UnfollowIndexAction.Request(); + unfollowRequest.setFollowIndex(index); + client().execute(UnfollowIndexAction.INSTANCE, unfollowRequest).get(); + } assertBusy(() -> { final ClusterState clusterState = client().admin().cluster().prepareState().get().getState(); final PersistentTasksCustomMetaData tasks = clusterState.getMetaData().custom(PersistentTasksCustomMetaData.TYPE); diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java index d671bbd1875..f168bccc8ca 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/TransportFollowIndexActionTests.java @@ -20,6 +20,7 @@ import org.elasticsearch.xpack.ccr.ShardChangesIT; import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; +import java.util.HashMap; import java.util.Map; import static java.util.Collections.emptyMap; @@ -29,10 +30,11 @@ import static org.hamcrest.Matchers.equalTo; public class TransportFollowIndexActionTests extends ESTestCase { - private static final Map CUSTOM_METADATA = - singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "uuid"); - public void testValidation() throws IOException { + final Map customMetaData = new HashMap<>(); + customMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "uuid"); + customMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "_na_"); + FollowIndexAction.Request request = ShardChangesIT.createFollowRequest("index1", "index2"); String[] UUIDs = new String[]{"uuid"}; { @@ -47,12 +49,23 @@ public class TransportFollowIndexActionTests extends ESTestCase { () -> validate(request, leaderIMD, null, null, null)); assertThat(e.getMessage(), equalTo("follow index [index2] does not exist")); } + { + // should fail because the recorded leader index uuid is not equal to the leader actual index + IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, customMetaData); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, + singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "another-value")); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, followIMD, UUIDs, null)); + assertThat(e.getMessage(), equalTo("follow index [index2] should reference [_na_] as leader index but " + + "instead reference [another-value] as leader index")); + } { // should fail because the recorded leader index history uuid is not equal to the leader actual index history uuid: IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); - Map customMetaData = - singletonMap(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "another-uuid"); - IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, customMetaData); + Map anotherCustomMetaData = new HashMap<>(); + anotherCustomMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_UUID_KEY, "_na_"); + anotherCustomMetaData.put(Ccr.CCR_CUSTOM_METADATA_LEADER_INDEX_SHARD_HISTORY_UUIDS, "another-uuid"); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, anotherCustomMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader shard [index2][0] should reference [another-uuid] as history uuid but " + @@ -61,7 +74,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { { // should fail because leader index does not have soft deletes enabled IndexMetaData leaderIMD = createIMD("index1", 5, Settings.EMPTY, emptyMap()); - IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 5, Settings.EMPTY, customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index [index1] does not have soft deletes enabled")); } @@ -69,7 +82,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { // should fail because the number of primary shards between leader and follow index are not equal IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); - IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 4, Settings.EMPTY, customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader index primary shards [5] does not match with the number of shards of the follow index [4]")); @@ -79,16 +92,28 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", State.CLOSE, "{}", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{}", 5, Settings.builder() - .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), CUSTOM_METADATA); + .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("leader and follow index must be open")); } + { + // should fail, because index.xpack.ccr.following_index setting has not been enabled in leader index + IndexMetaData leaderIMD = createIMD("index1", 1, + Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), customMetaData); + IndexMetaData followIMD = createIMD("index2", 1, Settings.EMPTY, customMetaData); + MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); + mapperService.updateMapping(null, followIMD); + Exception e = expectThrows(IllegalArgumentException.class, + () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); + assertThat(e.getMessage(), equalTo("the following index [index2] is not ready to follow; " + + "the setting [index.xpack.ccr.following_index] must be enabled.")); + } { // should fail, because leader has a field with the same name mapped as keyword and follower as text IndexMetaData leaderIMD = createIMD("index1", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"keyword\"}}}", 5, Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", State.OPEN, "{\"properties\": {\"field\": {\"type\": \"text\"}}}", 5, - Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); + Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, mapperService)); @@ -104,7 +129,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); Exception e = expectThrows(IllegalArgumentException.class, () -> validate(request, leaderIMD, followIMD, UUIDs, null)); assertThat(e.getMessage(), equalTo("the leader and follower index settings must be identical")); } @@ -114,7 +139,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { Settings.builder().put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); Settings followingIndexSettings = randomBoolean() ? Settings.EMPTY : Settings.builder().put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), false).build(); - IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings, CUSTOM_METADATA); + IndexMetaData followIMD = createIMD("index2", 5, followingIndexSettings, customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followingIndexSettings, "index2"); mapperService.updateMapping(null, followIMD); @@ -128,7 +153,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData leaderIMD = createIMD("index1", 5, Settings.builder() .put(IndexSettings.INDEX_SOFT_DELETES_SETTING.getKey(), "true").build(), emptyMap()); IndexMetaData followIMD = createIMD("index2", 5, Settings.builder() - .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), CUSTOM_METADATA); + .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true).build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), Settings.EMPTY, "index2"); mapperService.updateMapping(null, followIMD); validate(request, leaderIMD, followIMD, UUIDs, mapperService); @@ -143,7 +168,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { IndexMetaData followIMD = createIMD("index2", State.OPEN, mapping, 5, Settings.builder() .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); @@ -161,7 +186,7 @@ public class TransportFollowIndexActionTests extends ESTestCase { .put(CcrSettings.CCR_FOLLOWING_INDEX_SETTING.getKey(), true) .put(IndexSettings.INDEX_REFRESH_INTERVAL_SETTING.getKey(), "10s") .put("index.analysis.analyzer.my_analyzer.type", "custom") - .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), CUSTOM_METADATA); + .put("index.analysis.analyzer.my_analyzer.tokenizer", "standard").build(), customMetaData); MapperService mapperService = MapperTestUtils.newMapperService(xContentRegistry(), createTempDir(), followIMD.getSettings(), "index2"); mapperService.updateMapping(null, followIMD); From 6dfe54c8381e2b9046de836fb07fabaa03a02452 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 13 Sep 2018 06:35:36 -0400 Subject: [PATCH 02/45] Use serializable exception in GCP listeners (#33657) We used TimeoutException here but that's not serializable. This commit switches to a serializable exception so that we can test for the exception type on the remote side. --- .../index/shard/GlobalCheckpointListeners.java | 13 +++++++------ .../index/shard/GlobalCheckpointListenersTests.java | 9 +++++---- .../org/elasticsearch/index/shard/IndexShardIT.java | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index 224d5be17e1..e738ebac160 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -33,7 +34,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -53,7 +53,8 @@ public class GlobalCheckpointListeners implements Closeable { * Callback when the global checkpoint is updated or the shard is closed. If the shard is closed, the value of the global checkpoint * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null and an * instance of {@link IndexShardClosedException }. If the listener timed out waiting for notification then the exception will be - * non-null and an instance of {@link TimeoutException}. If the global checkpoint is updated, the exception will be null. + * non-null and an instance of {@link ElasticsearchTimeoutException}. If the global checkpoint is updated, the exception will be + * null. * * @param globalCheckpoint the updated global checkpoint * @param e if non-null, the shard is closed or the listener timed out @@ -96,8 +97,8 @@ public class GlobalCheckpointListeners implements Closeable { * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be - * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for - * the timeout means no timeout will be associated to the listener. + * notified after if the timeout elapses. In this case, the listener will be notified with a {@link ElasticsearchTimeoutException}. + * Passing null for the timeout means no timeout will be associated to the listener. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener @@ -140,7 +141,7 @@ public class GlobalCheckpointListeners implements Closeable { removed = listeners != null && listeners.remove(listener) != null; } if (removed) { - final TimeoutException e = new TimeoutException(timeout.getStringRep()); + final ElasticsearchTimeoutException e = new ElasticsearchTimeoutException(timeout.getStringRep()); logger.trace("global checkpoint listener timed out", e); executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); } @@ -225,7 +226,7 @@ public class GlobalCheckpointListeners implements Closeable { } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); } else { - assert e instanceof TimeoutException : e; + assert e instanceof ElasticsearchTimeoutException : e; logger.warn("error notifying global checkpoint listener of timeout", caught); } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index e5e2453682f..8a1070d56d5 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -42,7 +43,6 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -512,10 +512,11 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); assertThat(e, hasToString(containsString(timeout.getStringRep()))); final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor t = ArgumentCaptor.forClass(TimeoutException.class); + final ArgumentCaptor t = + ArgumentCaptor.forClass(ElasticsearchTimeoutException.class); verify(mockLogger).trace(message.capture(), t.capture()); assertThat(message.getValue(), equalTo("global checkpoint listener timed out")); assertThat(t.getValue(), hasToString(containsString(timeout.getStringRep()))); @@ -547,7 +548,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); } finally { latch.countDown(); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 8fe1daefe6d..715860e6ffa 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.index.shard; import org.apache.lucene.store.LockObtainFailedException; +import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -89,7 +90,6 @@ import java.util.Locale; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; -import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -798,7 +798,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); assertNotNull(e); - assertThat(e, instanceOf(TimeoutException.class)); + assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); assertThat(e.getMessage(), equalTo(timeout.getStringRep())); } finally { latch.countDown(); From d806a0e59d1cf0730c5f089b551385f667bad26e Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Thu, 13 Sep 2018 07:00:40 -0400 Subject: [PATCH 03/45] Fix race in global checkpoint listeners test This race can occur if the latch from the listener notifies the test thread and the test thread races ahead before the scheduler thread has a chance to emit the log message. This commit fixes this test by not counting down the latch until after the log message we are going to assert on has been emitted. --- .../shard/GlobalCheckpointListenersTests.java | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 8a1070d56d5..4a6383c50d2 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -49,10 +49,13 @@ import java.util.concurrent.atomic.AtomicLong; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; +import static org.hamcrest.Matchers.any; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Matchers.argThat; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; @@ -561,19 +564,19 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } public void testFailingListenerAfterTimeout() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); final Logger mockLogger = mock(Logger.class); + doAnswer(invocationOnMock -> { + latch.countDown(); + return null; + }).when(mockLogger).warn(argThat(any(String.class)), argThat(any(RuntimeException.class))); final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, mockLogger); - final CountDownLatch latch = new CountDownLatch(1); final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); globalCheckpointListeners.add( NO_OPS_PERFORMED, (g, e) -> { - try { - throw new RuntimeException("failure"); - } finally { - latch.countDown(); - } + throw new RuntimeException("failure"); }, timeout); latch.await(); From a192785fc84383aac8837a8467cc302dc0523ccf Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Sat, 8 Sep 2018 16:23:43 +0300 Subject: [PATCH 04/45] DOC: Add SQL section on client applications Add setup instructions for a number of GUI SQL applications --- .../sql/client-apps/dbeaver-1-new-conn.png | Bin 0 -> 25260 bytes .../sql/client-apps/dbeaver-2-conn-es.png | Bin 0 -> 23146 bytes .../sql/client-apps/dbeaver-3-conn-props.png | Bin 0 -> 20325 bytes .../sql/client-apps/dbeaver-4-driver-ver.png | Bin 0 -> 29626 bytes .../sql/client-apps/dbeaver-5-test-conn.png | Bin 0 -> 28780 bytes .../images/sql/client-apps/dbeaver-6-data.png | Bin 0 -> 87596 bytes .../client-apps/dbvis-1-driver-manager.png | Bin 0 -> 60954 bytes .../images/sql/client-apps/dbvis-2-driver.png | Bin 0 -> 57074 bytes .../sql/client-apps/dbvis-3-new-conn.png | Bin 0 -> 55342 bytes .../sql/client-apps/dbvis-4-conn-props.png | Bin 0 -> 57094 bytes .../images/sql/client-apps/dbvis-5-data.png | Bin 0 -> 99284 bytes .../client-apps/squirell-1-view-drivers.png | Bin 0 -> 22210 bytes .../sql/client-apps/squirell-2-new-driver.png | Bin 0 -> 27109 bytes .../sql/client-apps/squirell-3-add-driver.png | Bin 0 -> 17121 bytes .../client-apps/squirell-4-driver-list.png | Bin 0 -> 29004 bytes .../sql/client-apps/squirell-5-add-alias.png | Bin 0 -> 22199 bytes .../client-apps/squirell-6-alias-props.png | Bin 0 -> 13045 bytes .../sql/client-apps/squirell-7-data.png | Bin 0 -> 44964 bytes .../workbench-1-manage-drivers.png | Bin 0 -> 16439 bytes .../client-apps/workbench-2-add-driver.png | Bin 0 -> 26008 bytes .../client-apps/workbench-3-connection.png | Bin 0 -> 35138 bytes .../sql/client-apps/workbench-4-data.png | Bin 0 -> 75863 bytes .../endpoints/client-apps/dbeaver.asciidoc | 57 ++++++++++++++++++ .../sql/endpoints/client-apps/dbvis.asciidoc | 42 +++++++++++++ .../sql/endpoints/client-apps/index.asciidoc | 21 +++++++ .../endpoints/client-apps/squirrel.asciidoc | 50 +++++++++++++++ .../endpoints/client-apps/workbench.asciidoc | 40 ++++++++++++ docs/reference/sql/endpoints/index.asciidoc | 1 + docs/reference/sql/endpoints/jdbc.asciidoc | 12 +++- docs/reference/sql/index.asciidoc | 2 + .../language/syntax/describe-table.asciidoc | 7 ++- 31 files changed, 228 insertions(+), 4 deletions(-) create mode 100644 docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-4-driver-ver.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbeaver-6-data.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-2-driver.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-3-new-conn.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-4-conn-props.png create mode 100644 docs/reference/images/sql/client-apps/dbvis-5-data.png create mode 100644 docs/reference/images/sql/client-apps/squirell-1-view-drivers.png create mode 100644 docs/reference/images/sql/client-apps/squirell-2-new-driver.png create mode 100644 docs/reference/images/sql/client-apps/squirell-3-add-driver.png create mode 100644 docs/reference/images/sql/client-apps/squirell-4-driver-list.png create mode 100644 docs/reference/images/sql/client-apps/squirell-5-add-alias.png create mode 100644 docs/reference/images/sql/client-apps/squirell-6-alias-props.png create mode 100644 docs/reference/images/sql/client-apps/squirell-7-data.png create mode 100644 docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png create mode 100644 docs/reference/images/sql/client-apps/workbench-2-add-driver.png create mode 100644 docs/reference/images/sql/client-apps/workbench-3-connection.png create mode 100644 docs/reference/images/sql/client-apps/workbench-4-data.png create mode 100644 docs/reference/sql/endpoints/client-apps/dbeaver.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/dbvis.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/index.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/squirrel.asciidoc create mode 100644 docs/reference/sql/endpoints/client-apps/workbench.asciidoc diff --git a/docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png b/docs/reference/images/sql/client-apps/dbeaver-1-new-conn.png new file mode 100644 index 0000000000000000000000000000000000000000..2307f0393266361aa9ba3772126145be29e8cb84 GIT binary patch literal 25260 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 L;ZT9K*R_8D#VK=l literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png b/docs/reference/images/sql/client-apps/dbeaver-2-conn-es.png new file mode 100644 index 0000000000000000000000000000000000000000..1ca209a57e5553c35c1648825bada8d25daa389e GIT binary patch literal 23146 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 QWue2(*%S8S^p({A0Y25mLI3~& literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png b/docs/reference/images/sql/client-apps/dbeaver-3-conn-props.png new file mode 100644 index 0000000000000000000000000000000000000000..7561e94bdd9914399befe0c892a9a2f9268e4cb8 GIT binary patch literal 20325 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png b/docs/reference/images/sql/client-apps/dbeaver-5-test-conn.png new file mode 100644 index 0000000000000000000000000000000000000000..70f2a1dd4dc2f33e0e251323cd5c7b22d4279cd5 GIT binary patch literal 28780 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 yuW96Pe&h=0_miO#|Y7$fgL_kEO1O!BSjSzYX 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 O&(%w27t1c(eeyq89jxg9 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png b/docs/reference/images/sql/client-apps/dbvis-1-driver-manager.png new file mode 100644 index 0000000000000000000000000000000000000000..b0ff89cc9d75ae34907eab420427dcf109b39716 GIT binary patch literal 60954 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

wwHbO_Lz%yXpnC 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-4-conn-props.png b/docs/reference/images/sql/client-apps/dbvis-4-conn-props.png new file mode 100644 index 0000000000000000000000000000000000000000..2027949c401a76ec0e5ec1153f2fbae1a780e63a GIT binary patch literal 57094 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

ILJ13iO$}FTy9sF&V?WLOinG(2`wh|aLFq~ z33+>;&V^(bq1=8?yyQOgSa)t{whKkik2Dq!pPfYAtNXyK5p7be-Uh`t~A zq)OueI(qcRUNa+}q>kQUGT-K`AvK?{A0eh1GI^-#YY_IQzW$#uGie0w0+Fsw8vCCLY_i=$p zHO{4VIUY9jzmh`q%Q}kU4^hXm5#tP5LUmn80i`NmyaFrei!7*I9w|R}N=k9(qk#w@ zQO|e}9tuWogx_840|YeM(d=w&Utx!)$GAY$3yirVY%cTQ>h!-G3h6m;EL zq>Bz;v*BM1*sW#v@aJvWUtA02Z5nRbC5<%tht4jpO`i+P34XInq2n5@1CMfv<;^rZ zBEny3Kxyxfh^D$`(lg>MJvp^FglT?5aj{HE>Awe9&X)y}SMWs; z4jpS-R;|iPQCc0ttV)4fdvio>s8Ijb<_R~(*Ed`&@9#G|uHs|#IdoKgoH$}TUZ?g0 z21xt0vWz^Q!Zw#b1YE7plyCwj^JdNAm!{d^~H< zZNF}ijM-!{=jo$!q<26_Rqww;EI**<*p=i^$*Q>q!|we`m2kCZrJ?RpS3GK_WCJo( zLIQ5DamMix>LX+^YceUGq^y~}mo6d8`XOik0IkqRS^xp_0LB_+?(fejQFVw}yTmQl zH~|}r3ESOjSqND)_ZehoSLNt2R^2l9Ne#8~$(LlT;a=TR#}k9Av3^{;Eb0bv<@>S2 zTmoEEXjTDUEZ1$Td-wR*fY`4|T;6QSrJtD7rkWaK#ZDIsnj9^MYLw*I)h%Qd!{@`L zh{fjS+&yQUG9FCGaOF$g-vlkTe}_|Que7})ehsX1xA{%|)NvH~Pb0;|dC9m6=gi40 z#Fa*;0E3yR8Jlv)K2zC>eWrFX?#>mh8ZZ4y%`5p*7)zo1vf>4mC{A&~*SCSK`l~1R zT#r>Xj_JKbydiJl(_rEXcW37oFO5u5tvNs~R2lUHiF9TFiVBAM&925KMJ7chMJL7R zUlF*{E;SC#2x*Qqyq{}|i}{L)>G!pmxJ#BddX6YB>emU`6 zo@68HWygR^>C5?&Jcv*%e`d6ka3QdUiO{0LV`76-K5Ro&`>D>VHO)$OqB*$`sl?R& z5Z@Z`z1)pqW+&iP^!sA3y-hVqgp`@~JZ~U2eoH(beoMoys9e=wM9a~q8TfwfwJY3} zU=;%n9i=Uk^Raoq8QwDpE=BbFG`S1sEpqx9T-7PbD@yLrcNZ=w$~XzwVkLcR2B*cd z`!_bPy~fDNu=U?6k5=0h(05VNxf%;@MkN>6zU~qN)=Kzkr}pDiho)o0wMNbJO1>O` z55Zr|HejfUzhfqnU-W8z4v6d@ufE(jTk?Q>cO%A8IJ@>5;QmhD*y;@wr6vV(aEs*O z!5UkL0YhyAxl_kf(+t0r8f3oYRrGkCug2mmw@6MPM^DqE+thES^X|MZPwePS&aV+I z&FAX+l@Ket)cq|fZKDoGg@^!Lm%}Ok!+(KyXzJ{`w{6i~E zT(nan7LZRSwC)&bGcMI1+kA1}vT~TOt*dC2YxCLjgRk#|d0z#20gsI9QJSib5cG=S zt8Hoe>4HM%EV4}WF~DD#(B`Ft@T$|pjkeynA{{efT|whuU_(r<V;_I~^G=&H}*y-?n%x8z?v(4>0l#bSm@+6vUmklP$|P3w*?+p?Y7X?9Rid F{|n#@SjYeX literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/dbvis-5-data.png b/docs/reference/images/sql/client-apps/dbvis-5-data.png new file mode 100644 index 0000000000000000000000000000000000000000..fb5ce8b86aa74d48a6139836bb2c2a5e845c9cb6 GIT binary patch literal 99284 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 M`vjr*UT1#ze;WesHvj+t literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-1-view-drivers.png b/docs/reference/images/sql/client-apps/squirell-1-view-drivers.png new file mode 100644 index 0000000000000000000000000000000000000000..22abbbed741ee2c9f781a31152ba348ef2c5f6d7 GIT binary patch literal 22210 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 TgWch5=t{(qB1AwbB3-0PRa&G3R04867q$Ltc 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 Y@!d0}W_4$rH~(vFc>OZr(#`w-1#~{RR{#J2 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-3-add-driver.png b/docs/reference/images/sql/client-apps/squirell-3-add-driver.png new file mode 100644 index 0000000000000000000000000000000000000000..9a9c2c2634e3cbe73d56dd2dfc1a7ff2de9b9828 GIT binary patch literal 17121 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 Kkix6?VE+%re>X?~ literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-4-driver-list.png b/docs/reference/images/sql/client-apps/squirell-4-driver-list.png new file mode 100644 index 0000000000000000000000000000000000000000..35f389747c9703a4ce4dd83eeed44a24abdaa1f4 GIT binary patch literal 29004 zcmd43XIPV2+cwJB3xeVxDk1_RN+=2fN*$$xfPxSa0;3R+k_0KCgfc@_6ln&eTM(rs z5+D(hfP&JCfDl6p(n}ILiS&J=^E@Ln@3Hqjj_>{P{eZx|*1guXu6333I@f~xSIrIg z?K!%KkB@Jkv5}r7AKw-tAKx!CyLJFiU`D>ez{f8>mWG%4Fm1=ifnT<{UNXDH$5#@+ zcjeZ0;P>5LMz%hDd;+x_U%%8NUOV&gdG#CXUAi9Nz#;GbA_uk|CAK%581Q;>#Ne3T zHKlFFlCSsIuKnsV_{+{mgGaEx88oo9q~`he_n&^UKjyBX!tRKXz1u$gc6K-6-u~>< z2~!`us3$b<{L%WyF<%_|zR0JYqrYDjII7rg`-*A3U{hVb1nDlXBv!5Z@utA-Vg8f? z>p}$xV|BU5ff2F`kdTirdZv2+%7spJDL>IP!{m*LMBWf&~{1~=; zX63A(kwvIaC-GOj1mh23#&vJM7|t&YsYdXDjV=krW)smpqE?G$FDXl}7d}V5**9;D z5w>Tzp?>3?Uv&R5%tA@%9dgH>H^P0H^EVyk^DKP3@x?XwJs94Glky=>J) z%0SvgCO*~Qt$)E;O&G#yEl(=!!Q3v~$+O6y*AM0VCFuQy-~4nzHL_JgAhLrZcF&Y0 zPD9TZW&ubmY^U`WoA2$IjFtYbKRXdt#6XNqq;;ruFwA1=YQI=ydPyuM*TT14hGK$g zDn9;22kTue>*(qo_G5)ACC5)YoDRJsePgA=1k0W0?#5OYG)&odI0CJ1L8uhJr%ttn;ok) zst0DI1vSzw!`nus;EOhOvKsAiEIJ6O1ir8sR_$nDwvOTjCCD}q_7y) zsjau66jnES(rNWH_> zm54(7nV$Cx4Eb^4QfKGuJiX z{u0#gnac6~I~KS6K7JF9@+-`YVE4D>*PDwCzdO0AS{%CxoOB-?>)3mj$c*`HqX2yG@+Dz>FY1u4BiASAa&}Dx&NbKU zOVv?cSt{2N3!T!!3L*jJ@3MHh0P7ad-jDr!mslZ~AE!W6y7BFythxg@ zbhpzRc6RXUT&?4JqhSrPZ+c}qsxQnUfGZec_wA*Kw(`nQ^(toCPK_)VJZ6UVqL})0 zI0-fyr;U_|w0^Fb4Ob@m323R>KmMGS8l*AIYBlwwP)F@;{75fp==pLGCoAwh8pbGT z11p5omo}8NHCG+$(~nmftwe>AKQDhCYS~+ZHqU}%_(d`UPit}|5k~+Ewyvwn=6$r| z`5JJ?r4~6&XdPHq}C`&C` z%>j9SrNyd!pJGTIFDjnt8~6>glMb?9Ew$K9fz0apuc|MI`pKRXf&AV#l%d`zT5UqR z6p$>gaJx{8sq~9yuh}pr=^V_eH$%qBRR06k;_LInL9>C^0@%L<7HzDaGczx4xO&)v z)s6}-K8zi#00j1XKw<_BtLjd${qxr{T%T@TKe;k069)dq=nJqyt3}&k0v+BiPUHxrYOzrC1t$#YL}uVvi`RQ} z8}8K1J}oJu{9~CI{**Rh@u{yIoBJ&8eHUT3sj?=x`HoT{QfQ-&%YwL6mQy^%bmT3~ z2Uiug3rH%JFTGC^=q^TCT?Z`O>-R$%zu*d=7wQOSu07RT5+ly4?4M=0a@5XUzv!RM zURxN|K4`wyTUcush7<~2>(i25Ne!F(G~M|s)b;JxKY4o;zx`QS3!4hI{`P@cSW9$R zB~34hi22c8D^JB$I#j2P9|EHkCQ%VnPdagn%L$sH*Z5so?K)MmSs~*(brLgpTSJ;EGz3R; z`Ql`{OTu}vfmH>DE9bV2$D(N^df?86$qH+>n$kVQ1SS4uUwOvb8@(xu9q|gpFyCdF zTErAYl=&^(slRhd2V_4Rsa)$LZZ~|l@0ddHXSFEt(68iLzXRT&j)OMwH`DlJUysR{J*C?eP zKdN2Tpdb;+1`n;$K!hWfqrL{x{&F4YA~L&bVY^|iX-)t6w39JoK$RBwOrwUTi7 zb|?kjF7ML|o5=s2KUpXleGKs)xw8y3o}J?x$_(^sgUC+d#C<-VrJBEr6J|#=t_&7- z+Ey2>s&~RyZwMydUUfR{@=3*-5Dx^z9(j%jBQt=Njp3)}MBv-q5*Qb8%lG|BXGp6b zMWP*pd#VL=D(3d}%dGJbg6re-utRIB6^`QBt4|PnM8ek7`Y&2s0NGE!!3tWe_Z=4s zoy|9-BXRdFZ6NfeQ#x1I+!wu^r4J-x=r@czFF-3T%*(RN>`oHXUp^xIPTbmy_HDON z(&1@d7*)s)n|op;x;3&LuGZu@y7KF!u9a%Baw~f#aJ0bzy!BCuVhNmg%%Y3^Dp1oE zl}S&C2h zHe#In2j#y^{wBf7&UvQv`pAj!tx*4)if%rMA)h{k92jr47o_4jekxxNk4h872qb2< zVtslbg?}UT{p7CjdLd?c3PX^@4S2icwys1P+S$(E9(o0BsB>C#`EK9g?3H3b7>;I6*)gys6mRLz%fz=Sv{-y@eMxWTq(+>&aYyV*RsH~Y=O?B%Vl zQ6gH4w3?Zzu&c*#(~o_sRD=7rR;y;Bs!>GN{Hwr-sSl~oY(mwI`&M8mYmICB^EiIP zbq7QUMdp>Ow#Fs{+}|%3En^^GJhK^Mc!UrBl~5jpJ|IFWng8zAt2n zJIX%^UAc;Cpil=_rux((i&QAZqMRET_r=mFamW{IM zarpPocXj_?pSWLeM3GZ50*#l-s8lWCnta9^pF1)QjLJt$N9i=!=oq`}{a`Z1@vs7E zEx9#ww$W=fDmBJ#t*mnS6(O>DVeL=JkYntN4)2|+`{HRbSV5h&L7n41{ZSNf{JT9D z_Qf-pLsy@$Rg@2mmVC*G)U|Z$_tEU}L0$7+R(94A>@DHr^V}gSNqogJI|&1ifT$By zwb-4Kx~Y!hm}*U7Nh#val*vIacYm2>Kf`MtWyd!tGl7a6&Y#eXkp2uiYVr z?Qe4GUfwxiFVH)fLCcX?u5h8q^1Vy{gRKzN1rOHExo+C?K!-d$>lFws3(|nDIukT> zdYp8Ga!A_t0gLkezvWCmYw&rt`r^eQS|i-LXU}y08)7Nj$>-DYKKdFzwOxg1^9x^3 zrF1urL?Io-m_Svs< zT|r^kqE^25g(xbZ*wdEEiN*4x?yM8ZVrwU0X3eiYb~@RnTC96$TM?ej-+ePzv3ywx z(Y9&j`yl(fXE%e+MVS0K_*)-{T709!!cIHQ)T}v`>0@fmz8&nr^AMZ8FH4Ij z*36}uOZM(DmPJD|9$ZEo!-A|>%DtGK8PO*G&P9xm?{EuufqhC#=iJ`v_klD#*>-%^ zH>XAPEv?Xlr|4FgPidMG%GDKd4wW^{Kj{}`idt9rJeyblrc3n6yky-{XBF6AVu=35 zCvVi+s6B9GCSi`c2bW-nFc$>{U6E{Eyf3KVc+t1LFpxbki+N|F7_yom6F<`uEFqg< zaMD`AuJPfonS=DfU)!rg*wE6rmGp(@o~H3`nQHA6v}Wjd6=*!PmsQ@>;uDO|q6)73 z!uM`J{yLI~xc6Ytlh_qnNz$0A8h7QjQ*EM(?6O`+Gq>rxR0;{}7oa6T8W(Swpdz3% z!zEeyuZK}PHT*Q-+Td(>t6A7qO`Bi*D^P zkIj&+;!!p4eCWIj7u&P)Po&@awRp(J&$Tz%B_(jBGX%WEE3Lz1;GNd%0glDwi#9cm z2ku@@ffM1VrRX7fwYix%q$PQ=SFMT;7_r%SgZG))zR$|y_X^fULu^7@LWmBJU)f3s zQ43HaQ1aSwx*J2kAtz%Jr*}A)Py(^Z@dx%W9x}oZS%E38JPDpXT#BuPd7#R4bioVxMcl z_gTq+8u<9m#>+ou#D1$wToW0P{ERSyX(%rzPTHyW-TSRF#D6`>@5O7ktdD)$UK+L^ zJe`;ubXr%|I4DdVGk-+CLRoZNh4@tu0(1mC z`{c7^??sEIPd;PW6up~fcAWi>jbsVq!56O?B{QwY?{XY?TifquwrZIRXN^9By^vHe zWi4?O*1o-5#+)4X$r)T&UD}%Omlfrh$H#}y+?N3P>4{LY47QoB@tt_3)1Xk2s5T!T!gh*GIN6{;#R3B*!4nt} z2SG%|(3uCdsnsjPZBF`l5Ri>R6BujLYbBLMFWE{i{;Ca=?;8)}K!`m@5f?AMy@cF7 zQ_HdqOJH~I_ZMdE2GePb+h~p%VM)NXfX45I8Q+0OuJI>3UfeCre)uf?b z^y4zJELFH;745i)-YP|sEJuW}>|AGiDOIZWfy3xY^=20sG}SkPeLPc6k~q61VCePy z*OAg9cFG5j2)RRnMI^PL%Xlqa$9g?){hlGDaLp;2_2PVrqGaAt7|o7=NJ|Zz5(h*4 ztYxr4-Gw!ZYXgb~Zo##p%>7bxHb{|+VbwE#3YN{Jq}C-vXvB;`FUWctnFDzUmgX*bNs2s1P5cAa)jHEJqj@&>4}OaJ-VMs zo{2-S^|rMYYMj5=Xr^42ZSbhR$+NObq110lX=+tBmz(Z^*^NapShq1`ZtpjbTi%aC z7gNJ7l9sx$0*Jt7I~U1e2UNwqq0k|xoI6lT6ZRD3m7?Su*|*;O-r8kyagVXj3H}>a z@Emctwz0XbP=I~oZ3pQtBcMp5I%gau1^JjEheLTOT}Q?r&*6^XFVk9B-{|J)cZst# z(`&ryKA5t|fg6E$#gZ=JM6pz-{x5-}QO)LDM?zsrgT@r;+%b*w40xtaTv$WfgG{Tb zxa_qpasqY99KN_RKNam+56yEi62H)mPm6%s4gV zBgo-gS1D-J)cKls?LM_K9vQzRsf6m%2qWKc2hjXHG9U<#UzvRSVv-{U7+rm?eW997`-8~^ zdKf3H-eSc$SUZIMLNOl$^?}tdwD^?3oVY;*KUkoBiY;MnkZ=a^K|}t*@@KiZ?1+*g zUirH)c$3q}hAX%&{wA&K0L*);J#Kz+K>vfyb_$a>(Pk=}Ci_eX5f*KfUsvcsvuNg! zQYEyAoe>!i_#p}RwpA~-A|4Ss=UC+`iiJXu`+JJ$d0IsLl~H|P6$g|q4v*7NqA z=zF+eCR^1nMl1>O@#rHt@6ub&V?SPBq^Tp`E*X3)(ZTl7%c~4h+ga3Bj-#C*l|cz! z>dFzY2n3Sj?{KHM=gGS#Sl;AvM8{V$Bq1x4;R>51JMVr~R|Rvzk}*RC%OCzd}lM+oxQ5W&Vxk^=Xnk_|(Fn4~l#7hR%9Y z2p&*Atp(Lo){x%V(NAa}C2-&i)Ng zAXED+ld1xH5SCqPmF5+snFVxn227@ubElItlJat4fD~xeww)@bm_0qB2gcoF7ksI6 zu&IDaDpqEXnUGXrCNakR94f~id+n^~OsoB11P{R!VnzwE zlfq`LOS`STW3snbqQPDTw5j0Kv)6M5@VD3Rxh#1g*wH>L9g3l#neVi~;y952W34^xGqGa17 z-b}n2*x${MhQJ*4DF)lYZ(yb)jquUx^^CGw#EAWJ)^dr=4GhObQ<<`!a3>x*x%2JmE@TaMx-^&z$ses+f)}Ddaf_P!Fsf)AXA3HTHAU zgWF3lgdQ|t;JVP8qgG4K6k^pv^&Bc@dTfEA88*LolEN#Q2+SW0?PB$rdcFuWFPjT# zZ**TQ!LJ7GsxyTL-M1SlSDsS2JTe#(eQv+diyGSZy^@y1NU9xOe2Nx(0rCb7O9pup z3$hu$Vb0pceIhi2%51pqq#z97@0tQ-(TuXx;5#N!cttlwOy+~KhuMtq&ov%FPt`kL z53gmsX&ZI+U)tf?+y0o9k@rZsOnj{43zV_&D%?I*iug)dvI1Dt5m{pKSJjfarSLNZ zU`5{5Lnx$^T^t(^1J70NW0)^(JxIoRVAk<+dkS>;CCJ=`t?pT)R*fKUB2I@2bmU7VXTI9t3QLL@c}LHCSr06^5pkY(2)=mv9CYB8Db)Y< zhep-nJ58(f(0Tc}=P+Kea_%D+TdJ_}V>@9;8Z6 zMfA|LiHc8Yyb0QCx*h_g?quh~uVT12zx zhg1fBBiDjb873wgz?1kQa9>>$hC{XP5L*g2O?i;%`aRFIKgV8bZo+tiPc|jn5<5&b+x(^ew0vNG1F4b~lhtGQb(dk0Fhub41!9 zXWmz5Ae1DEH2Ah)z=~#|w`S~>=ZXWln%)@lca-s|Y@}4UzJNKY{+_4n(7vqw1NEr3{kRDLM}9xU*{`Jd zUmgRXD6|KUw4eCFW_U%GCou@5Vfket2`$^mB3Ulh3~_OT@<@rtA#ob2Hc0XHBLDq3 z9BJS>sm#Th$E6{#A^=E!XW4)%sH&F*pp(rZo2=_d{j0ld0lEZe`R4hU9puO>^UQ@< zAZ4%;&J`Qrns2c5E{+xQZ2kEqBteENnEA{W32_F7`aL*-k!^(PRKO7z&ybPVZ=~9a zY~b(1z!GITv&&eGlOcj)Ny(o_B-*ouPd9csmWfWH>fiyL-D}?7XK}CJD3mJT_5uZp zb_3iZSIdophDg1+b_W%idTb?t(#B#g+4ZOP1u6y4D6YG^r0wy5Gpfcae~5a5sFldg zWd!0t9mnZ1Rd1@aR_=_?WJT7i&6SYS!@~E>cBA?wT1idbe)iVLntl;8S{E{xnVkK$ z>vMjRXO5i6RPSP`V(>*0>%*(sun-Qa){eJk_jY1I{2niCCR(t(Cf$ypSh-0+{{*(` z?9t(#`PtrE{1&f2l*@;4PaX@JcQ6cst=@c;jJ!gY(F_X8k;|FyU5wZ9vHDhQv2yD% zjkaDtTAQ=Fn0i}q%8t1(GyOh3^0-uBwj z>fB`Tj*7b8;GFiA_Whw#a}xB0^{8pJF!37EFwS7VHDcxWfW|^ZoK^*8r75OX6uxvP zpe`&Bw4u{C27rdnBtd9W8Oe-7i>O-Hdqp+_1yGj?JQ_neV;E$3-=A}gJl5L39n3sd zHJY!ucYUeeLG1YY6tdP|vw!{J^TPj01KI3?KbjIEu3L5m9`RpJFtn%%$q8_GfuvCa zIGVW-!`zC=BY0NA$3cjOMxXz+Z_ixUECXsvz>$W*?WO9?*Kc(B#UIm3GTW?J3YzhS zSdbPws!5HTGsb#CTzU&9+UNHZv?~3sr!=|`Ks5ShUx$fQafms`*Ww#hl)sNetN&#D zkAadQ!eY21|C2dBV8cTX3HLyI^pU^t@znt})BQ3TjwCTd95}ryWpA=8h;!=01OK)0 zA?MTt>+hxIVG+Qzfg|+H{kW!GOD7+H_m9G@z)&*8Alxon>unnG8?|PU0MHMp2;+Al zk8ild4KcvN0gNGjfUWs`chvDFNb#jlG0`B*TS;bh-|D1D)c~kUk~AHqn3@0T5>icP zqk?42T0@PN!?O%GM7F87X~Z-X)2jx>rgp^#e2}VnrL#Kj6_27j5wL3y2~iu>y0 zkKavAq<$AHqt&KyQfn%(eR?K7Of%E3$F{%64zaI#eYskE5=kId_(`))5e*+5+b8o# ze0R4_L?rmb1rh9;QUwQ3h?%B7_bhiszPwmII zGY>ICX|VnRS&P8Mxc+MUIaPo1YJ(&$11QI`FWkL-D!e%2yBZ%ACowwZqyQO@=iCP) zgpKvVlY%LXZJd|$1LfxwB`YNHoe|4a6K{Q_v}C0u*`gR;JvBVFt38CFL0=dheq_?3 zsO5W_iHD+p%Y#+`=c0q%i=UUhk8iWM5z_$of>WxpbtC9B%OwuBe0KAN)(Yq z`$R}0t=$HP>b4d!>_;!x&_t=Le~O06Wb!)wtw)z-30d~neG8L6H5K2R^y?1x+b$@? z1VY$zRwHJo)<&$|8DE?PIl$nNr$UO^+3J4oW#@>JM9IqZbIt%b)g_=~X=488ajd68 z_L8K#S1|JUXP?#ORx@_J;SYpy1Z z%ZM$7ME}QhAdUUCF-{1KUX=%>@CATJ_3w;Ak5D1#F_Wbrq@b|{+&Zu0IMb49-H&c~ z6IF;2Y7B8Fk#7X~>3e59=CVzIy9K9YE0sbicJ-&w*Dj{|+skZmkK_u-$P@&1S5N?& z17;_TJo1-EnLJqL79G#U(!*BFr=} z7!FOhi8PBvAqzQ1+lc%>f`pwk(P`b@>sX=P{Z}rdNd{hpc6L=jb#x&AAVmy2#Hu`9 z(Hx6$W@kgDVoqk0P)-5E5#pL%FF?A7O~QVc1TN+5!qDAQj+S~vf?s~FdjTNu{{$q! zlOU_V_lB<6dp%|t_@<7xqV2<0Gu(F<>+v2PM6+!=K{e%9GLlMANa+t+-d_Lw?A zYOGlD*z|S~Uh8>cA<03ylC0PvjHR!% zBz`JveObd6+KxH3XX+!Q5n|)3B3YXMIS5+Ol6>tV#Rj>&cKEO)G5=)YX`1@?j6MNa z2%|k`Z_2YhlDX;n)$l8!l>~JraQ{XN3wP6_>wP+})Fi9JfEz}vy~V<`fV z=>cQT{th(%c;JFFG$9By%j&kX)*I<$I%McW zc~4#Lv3uuN&z;pnS{d71!~Y!#mRCk~bRN-)dk~2Fqjhfz8njL7QtaazGL2}Y02apS zP$Fb1_)h1NZm-Bs0e!gkPW39ST5LNw^!h-r2X>!D*isjxGAZH&(V70Z2@X`5K&0tv z2EsUZfVs@%2~^K%R3DsTshtnli1^?2;8F?%qMT*A3{!h;CSGuZ4|~hO(`vwB3*h$8 z04wOMBl#updv5Xr)ZeQ)aV>t2VDQ(iwJ|y?xBM%%QKgp;yl<1*h$e+j9#DUy&3WfC z61b7{u-(jR3C6ETS{aTkIv=jRTl&WyJar|=T%>NlI^S4&=ew|XsR7i5ARpMF@y$<{ zvKf6Vt$n`Dz>6)qIhqf4d`Et%#g7?+q07V@X+eddVTwLA%?1KdpKX zRk#E#o)AE7c2G@c7mOJI39|o=N?53t&W(T<0cJJMt}<*)ClY)n0-&KtrhA6-8CetI zncIksUHhifv+^dEtNnxF(QC?9-ypW%C}ABd}qs ztXdiEncajcXP}O3O##UJb}CXH7m;Tw;*uryRs@?66r zAaTP-?ov&t<%?s6Iuj|Tu}}&>WPff;)8ve-t$HUk=z^=WoNdp%N!{x&FK1IyROV#= zrxRB`ChciRu%M)Sbx1(I}-fv81aqYlNkT54&-wkHwP6ym~@DSdQq%c%03}v0e=` zgMs{6VKvUVJ)|v7jgR8W5ZZp5Z+tQH;m~(*6aoNmfF@8n1dhZjI}j=m`f55Zlp{e; zb@y2wX@1Q5^aqWMSHgkU3DsR0?rDjah~_S&NKf+}>Ea`}1j=L}+cy+0%aUE{q5dR4`HN`OPY;Mn~}1yyp3> z?t#>d9#}n2c6_!hwc9q#;L|AVMS64SuHsvHqjnGRRC8C9F0a0Y6=*-a^K+n4o`bdldzw=&a3XojV zpKj`+?<|7f8!{_*^n0+f2a3fL#Iu6`da$@St}jJ7^dmZahsjT3WJ+}ZR~`XzdeH&0 z{6feytbJ1U0;8`RrH-#=tNO5HhO<8i#M#?@k9%)7YNh{#4k?`@Mb*z$`4_45B^fwa zHoEy(c1eqB6n)9K;vaIFID_@oyjP5N85SLgi0Zow zozmFqjERJPP=tU-Or!kKH1Q@S+OOXKmSDqZu1myXj3)0NEGZK1b9X=mjP7&0P)aaFUjljqWB_gLJH zF~G?Gce?-*3AiMh1l*gByZ&4w_(DX(O(_RdvR>K4ghrgn30HNSmn1w%?*!jhQ?12t$jmve``8y&%9t9e>MXO?8WE!w5pK1ml5-TWpI{@8 zDikcZf#wxojoF_X%A+=b+r#F%#>oOHjz9`T+4oX{5{Ot|=dW0m{hui&PWrt!#$$~7*?v%)8AM6+RK z%Q73^^?2{kyAIGDkI>Kl<@^FrBPgh;-~8=BPXT@2-evfTQS;J7-h5dqL`R-CVn?-Z z%rZg5NYK0{iAfX2#RJ{#<^LQxl09h{naOUtVSk{vEr2mnCCpfP0&yMtb9E{JWS2HO z9@ga=eeR3u^)JsaXpr+$d>L)(X$#6U7P}B)oKi@C)#6$GK1GAam^(;F({MS{z7I9s zynSPTvVDJ;EN~ZY63ErUWfYqsn}bn=SUFMc$V>Usg_G~gwW=7$ugUkNU8$^b?dcGF zUCw#seP<%J!H+8ifuoYQyM_?d&gXYg4OA$Pz~*Vxs@#i;+`~!Bcg5N2x?Gt@tHsUg zqsm~1{*7%(7WkgpMS_q!4aSlomvU`_?!rc5CpC*2V6tY21@+@EHr&8M{fFJCUPjit zyk_rFx_)Up7(S;fvIDmr#|k|QGn+n%yfb7R^VcRfV9-(4uk?_&h~J<0Uj7k}UpVtV z#gkly6OoCKhVS$LWES<>%M{RF+}^pLU)fIO?EYVT)Cd5~zCw?%>$k_Js8L(v{^{aS z@DLYM8$)Zb*ulcS-lrR?N?8epiPNT=-97&JXXk&#gY8Hl9%$SWc>vj~d0*^k1^x%h zx`adlr+LuVmw$yM(hthqwgpvnlep7uqMaL8c-YrdKsq@ff(q)EcSFmA{o=j5!F2Z^ zq!U#l@KeE+0+n=}`y#yC22xd}(}+r#+0gS}`UI5Ic$Er6`GJ6|?Q|pQ%Gwy^hW*ib zhW?07v8Me{o0C1C-JH2xgJ^4K{U?HrSESw*f6ar{nEXS-#wG0ZXM4jR+fub0q)nx; zd2o8!bfy)hB;>mguqfhuHw>WK(`ERPs<3Yq~0- zRoXE5XYD_F>O#SF1F@fwbu3k0@z_W`yQi<_(-AA$5#$LiveLu%^WO^C+g{}!$H3|`a|&P@fa5Ww&#C@5TJZmn-2@IeoWh$y_j%8 zQTK4w98p~7hY&x3VOgqt>wml=e2U8O+|)*1W>j~|x4#6KWcZYN@Y}c*=QnGyy(b1N zeqpt**(CPu;)ae8&Jf9S%J2OK+V`|So{EIPo|YCZQv1yzb6p>lGxrcR%*a)o`U0Qc zg-6|%I7ds~`PE->KNSs>(q&UMa=SdwQj#cJnK)>8`NGZC9LILS157KTMz?WJBt}qG zsaJ-Gv~{e2(G>hy+}#NM(nr-#&+8IggvGBW zKihN7SKr$r^B%PLo6THrZH9Q~39Y<5UU!m7&Zoz%4SDk^vHRMJN<~fqW;Wd<*E*ZV z*;@gvL|7YhZ^&=$Wo8-0Pt6O4-(V)*@uA;M;|i(KEPB&0YRCRYJ9vyM;iN8BwDJDPIuz?WdwEiQy5(S?w2Ru$U!=;F0x(culCZj%-I0=1AoUqck*Q z(AwU<#A7Yqj@Edp z@KjUqZ>@>7H^4!PR<<6|pq7TGk7;<$?2-OtPU|9em5yXrriCOgMrl9op&*qMITi(6 zWN&iH#k@4w;}`#hqCmMlsTS5us6#mIr>-`Lhg>903s?Voe67I{AV4?84LOa-_g77y zE@F}8%85#LnDCU8!r#<|zy$I^%-e2E*Zda$-iC$Bo|-TDT?9D1?!l@3ykdX%bT17t$BOYv5slqSlgz>?&$ystrM0Y@>^A1eUYAnKN$zW9a+i}!}+bR zzDLulp6z|JwrLNR(Y$X3(`21_M6JAdbuA>@L)qNt20#Ga2Y)%p_3 z3KyJ-X4jB*H<|1~bWRekdY6Sieh%YLRae1r+Db6Dw*6!W8$qHOILv)ttf#gITS`iP zWUqSft;0n$!+ceAO)}CV1aFHy&{D86p5s0#YTUCd`(CWkw}%`l;xmkoum* zp9{w7B>3|jR4+a&8;(1R*ZWiWobbcUAeJ%2B9nIfiyqu5?W2*emC#IYIjx723w6j+ zdHDBkHQO{yb|c980;frLr)vB0I^Qh6Z8+UZ6H4b$vjb|dcv3wHxG*D&7ZSr;X52%-59?O0O``n}cV9VJ zsSyOTLfZE^z47Z@REgUpHHP0Vfk`j34+<1n{3Bh%g^ML47NX{Cb;PIgoGa5j8L_ridS@ZX|`v$@--4E_R^bOyBJ zzvSEo?3Bcz7&C+Y9}=Zt&Y^u5%0-b#WR|0oXakNF|1oLV8lp(H-SM~S1M~oJ1*U-m zi4i1Jyg0xtELD$wi^6mQdm6 z%O!g@?USV((Pb?B1aO`l55C*jn2EkbBqi2PXg;~%6zoLSOYa&EnLC55pc7~aN=EEA zX6}KW7Nok+q+EL&mX9&Dc_WSU|C{i+Ty~I>+Vvg6SmrK(P&=p9FdkGsl+GMYL$aVC z8g~0@jKudUa4&G8y;0D}VRpe>NpPRO*h!odl8C#rPOFy47l}DHb~fBoqqO$83s*Ps z)^2kRWme;?`#LgjpFbg^taLt9-pEf(UY_(fPWTt?MU~*MdB|3AhQ7Jnbkz1iN070t z{VS~4f%d$$Hs=Ahp?r^DR{kF*Q?O92#iutr=V*9|aqc$-ykqkVj$*_nV+UxP{rPAI z`;3a`#QW#9fLoGX5%)ItfPxdA?E!9RW{6e3d0!@eg|=mr!htia8styu^#_F^Nlf^O z8jnk2l!UV{3rq!_0qWHaDqK{8xP0IjYSgcw{0G~9@aN^JjMZ)bcme_KcG?L%n^OPJ ziw*Eg2@s;FH+8*8MgI#vi*+U>x8()<-wq0TS;R?Gfx@(CPAGtiztSw==xyw;flBszJ>IKT zPVDN@@Vz6$Rz<51SHfMz^ZW=0HZ9W|N8?V|;0nnVi6*`~_qmQWyl@eM!<$~|?&H>c zXfZs1PAkhcDiXE=TS_0fX1)8-!mHXd+*94!A4AUS%6Tr^-PDROKI>!Z^-Yl?5bV@d*%t4vn&&}1u(;i#o?KpO~O4Dlzj7qI%vvd?0{3efTCKg%4Hswu%<@}a*KLnuXv2_xjG|I$qwaRx46(3yVUyhIlpe_s_YcktHPTLf)vpTq0W_P!F^BnPnKpBdST z8Vw61@6;`vh$)C$GxW*)oxk7?rk^E%j)D7307V@>O5 zK8>Q!l+8G+f!=$mZ#E#z-~7o(s8_p!?qPwWnBs*TNRWUQ4BgOI@Z_dJ0O|i`L4a!J(i7n3BGP~29O+c>>X-NLHd&`ORanpc zr3l3ZGgfnR5F#V}<49yXRlY&Y3&jamu7O~tU=_RDZ$N$|9 z3Ba-LcT#TPx3G2$uNfvi)T0RVr>#wMlirMc!EW(6_|>k8I{fDRVwilw>VCyxQ($@{OA%BB$~j z6l5PXo4CMNJCUi3_^!=y<$n%>UmG24_$;CrS%2b0n?cu@`6!eO3Ws6lK$zqEQlJis zcVCfW;r>?_|3LQe|G%5nN2~?kOZd&nV#n?ZtrWHJ3bp*E)>YrYkHE2Gr6%{dMM0Qo zI;>|n?zEtSIJ2##1oO+L)wQ7iX4BsW;70d6HF`cSX6=o;)10+@4ao^oa%>!Lol#A6 zmA7Dx_IAvMBYPKBHf?i4;NMI+tu&*FdBxViH!4%E*LyS+%Wm_q81twHft40qCyF=E z4H)H5q@IGN3wRRv4Ou*VyT&Kx5AC$}URBosP(Q#@7g(05F^@9=4MQY`QQE1D`OLCx0?x=(r19f&wk= zN*-*a$&_ZRyvabv|E944(r>e(fTkam9}=Mawry(TPlS9?*fh}pzNP=mnT`N|){h^rfV@$Z{#w|OOvD2Cb5M!SaN!r+kc_{Ejibaism8J*;(Ft)v zSfY!^+j)~}o7VF$lLziePny+yHso!DMxzi4%<1^utOAZ$mxCPb48p^7Qy2yk)4z_Z zN_CF{SJh*zZc&=@Z?n{R{x4(j<1=RJ2#@q26cJ5V-DiTGbXIdK$qLhQiJ$PtN z634g4BPc^-S!K6cy;I3@%7MkL4>u*Ci$62i5z)%NA3o_tb#?A?LIid>fD%#37h(6E zZ7vtgwQ@K&nb#>xgnF*%88n`=iX@M7Ev%KqVoW|P6wz%O)NlSih26bL3!7ZywXy#Daaa`Z^F(sPRJK>ZAY5Zdn}+`i7kP{<6e;|bi5MFFZ$dN9^9R?m~vlbb8!E+ z$81Yi(wP6P>p-o+Ar{nJuz*r^HkZ_eL>G{{d}#iC>}1ZlY^sCxK12fsZqkk`sE zW>^LLJcbtgoIX}HBvfifW%{PA6k$D0M`@~r%^>Ta;A3o8{(ULpweuRYv=drYx1Z0z ze%zO!)>X*(GgweC*}9DBAU)bUCr7SKX5<#<#pwpw-_=TVhAm(J0-I_!G0Q@d zM>PGo9zt*o{sha6C+{F&$plFa(PXYRQhn{vjc8M89AGPN{y z(osuXrp!!pnKH#Ba7xikfkMF*ooRDvToTdLz!sO>APHd*&^t>>Ol>elL^N{?0nG(S zad|HDp6=;+-p~8~@%;B*|M20%#ksEYJLmki^E=-!3R$vQeE8`$`=(HS>Mx5^Nd*jJ z;d*g3r5m02n9&dAzM6`3HoQvD2qj_}N3%J&i-PcH(D5SCwtIq^-lWF{d^%^4Y$r~V zN%9K*AQ40x%%%?A3i|954{q%5doQP#3x%oRE!QcxV5qo|uYg?_v|Kbp!pH*NJk3AL z&#U^wO%ORDI8~KXsXt=sv@vi)y<*^i-MB*LzwqDcK~ zq1;B%x8roU!$NPi@%${szDbu9YIG=N(y(R{kGrmr@K*-mLn$_dw&;TgzuJvGxnf zD$H6}*q6pMw#F;rx}SJrfPwal1`5)zU7{5_HubmJ04Pk! zf}N3e=a*1QxmOHGLglz0{Lzrg#8qS~MEZlFjfJ)`!<}1wn%ZkKp@THLUvM$3@@=22 zhyP)#Qc3QH#R(^sl>zgSM;{rIZw2mr5Kz`8#&|AuDD`fUFR7Y6s$NE#Y>4&~_!AVO zdH;tgl;oG6cqSo#kE%;=v6$yg(GTxS%(M3I0ycu_OE;o}Mv_bla6dSZ{+{ptqykcC@^VEN}F!bE{s*SUlY;zJ;yAV9xxxpz&!I1Wh#yzU+Dd zG>j=y`$y|+e{9B3u1tH0`!!yjSCDKqy7Tn|P`I$5iObC$m%cz5n1(c(;M3-fQ>Y59 z%@P~5thcs++JOFh?%~tu{ap!J+g$=0^!wcA`p-~g2oEcLQ>y)d>(#$&M@mCchWD^0{+zBb+Z|ibV}o|8kLSf83FFkX7`zrOTNQ;D~EkrgPEQm#lkmXm7+)7hss(& z%b4Sm;Ci4KdbPY2cqQPcoXv;+&wm4Kxsi)eE56AqBW6+Kkc+`nzyF98Z~?52<~i)^ ze-+D3{@Hq@76Sj+D3EhcdA9bql5hU1s63o4ZhoWZT=5^Vg~Zgxa(yN~_U@lT=>CCe z{8}OY>KLCXm2KV)I035w8P&!M`c*qw1rKGLfkRIcOz8Lr)TP3`&ymKT1wX+6u(^+t zKLRKL5E~H*Pa`Wwk<5%&({H-hfSHQ$09w~Y;V+l#Fe$IrP&uTzLX*;a;_p{G>1OEC1YRW&i7U)nA6h{4P07AYzf6GR!~>NblNkfWm1%pG3CqncvF75tR`j zk(J!<&ne@4wS-qTsb30U$bvjemSr%*5?^iEIp~LQo6x5TZd|o$Wo8F=c-_1+1uu^i zZI23C=p)6DJ<9sAEe^KC8%UR|7#NP_5R>eOr&V;GD}q$*r`FV}7||0#jexI=>b(2;u{T<`vFHRy8UFrB9!hF|K-=$5)j#{{` z-Qx+e@E5q&I>(W-1kdu>#BYSk`&^Ln@@QXBG%I)tC3*GO`O$V;A1s`|(G6wdH}X|v zTbBu~Lu!@rqFf$}hmIGT*88yhWLDv3=BkN{1w=lsiyA%1kB335N);h65xyd1aMZue zB76+PN2jIUU^Fl+t>V;o)wiAW=_IEw1Ko1)JI)x3Zn>*m5;XTn7MQcU@#YM{Z`DOT z-K`)4tMGz6>E&`fy{2t$D;r}JND@uWIb?_%h%`M8N?<0=bX>8QEX0G95iSe87jZ;> zT#ebEv$=V3>RtF4y*RkN479znj;YnmxQPelTKqDSvXL!NT)!iq`}BZgAHvVg2$@So=FH2hYI5F z6KjZ&NKl|L16E`u#O2sqQQ&AZd^JjS(qx|?_?_~wXtsy24?0!{7u|xOsBI;VC}@HM zdg%i2dTO~>FoV)%$_N%w@#LvGOT5FEfffv$;6h&}F}lDzUfzeJ&Z3Lx!vuvq#k;rrq8~Gw}5Zh>jNETCmp`hOu(R8sl5%7d%Y*?z5WL<^M*+QBgD6TE?OkY`^lucw? z5&7~9ELhwi+q*t#JH)x>!Kgi|Jlc_oYLn%Xi;-Llm2wwCAnM4r$L8pq4-Z1ZXC~Uv z`|@9iObQ}74Xstd<8=s>uKno59HGsYKhyt8&|%sqsGxB8@>sZ4oI<`>3DXVX$J>w6 zrFD3-$0CMYq_&TP)5X!c_QeAeC5YCzT2zl!973W*u;V3k8ks1Y$)Tt8Jf5`|&YTr7 zG4Pq5Tk~g`p1~YDNg~-CVp3BaVNz2BA&*k^DE%D?zT`}Efzl?BJd5=Z;MGh-iL_i) z+s45i7b)mnz8d~dLwRJ_-dPJP4uv5>6VanqacwkD8Lh&h^otifru5obQg4X#1P~L`{O6;Z6^h6|t%Q&V$|Eec5%6QcD^{GjsdMPr<#Ie)LoJFLOJ^5v? z-1A5K2oPd46ch(YJWr@7B`w#M;Pvx?*634&QRP#Sf1t5K=G2BmCDSr%%+4~l>$1{(@@c+;O~QSlYTfx>JPL~)%EuXOz_~)@@Pg2?oxeiJqD8C=hEvkp>WRhExQv zAGB&+4lfWW@2h_?&;XUnOA+A(46-*ztnmt z2^p_z=SFa zp-$W#ZvSAdC@e55{CDZEg1yFNvQ>{l{H%2uv+tZ~agRGzaXBwO*fW^XNpr%p>8W=t zK-1fspZ$2@O82iatAG0;djI;$|LL*SJ$S(3`)~;Mv(I#a+V$1%|KESM_3_s5H+Q*E z{nz^ZGq3OYq)y{|*CXrCr~%*#)UIp4e?N7|4e-_1O~W>=-&JnutnhVqU4Q2CKd0u8 zt-6AY*^pSa&X-4?F9T>vqe%;Hl7xX-{Udqa@lf|~F z&9e`ef4KWoH^g(BC@Jt<=YM$I1!gkFT_2b@7G91mDSV}FM_T?DtS`XRAjo# z(29G4lJ6n5z#)I>f}7fvuT-mV3BJc2ubFHuD3b+eKD) zu&6a@DrIB@K5ip&w%oyzl|bC?k5916I*4}qH>Hw9aYyZ?VPcz}3l7<_F`u|&>a37uwtD zR|%@DS@QSJw z)|=J4l&$kyocjBqkU|}zavirr#lE1Io2x&Y&Amv-8gJDtH9y4YQhKP){53_48@H2` z?0h%j(yZkY;)PBjq{wn{bg(5p%qOWG;dTZrBt&1UljzAccRhS|GK8T1-wddOfZ#&?ij0Hstw+Nv2^-S&jN0<;-Mo@mHwpRT*yXaDew2iC{$);OtuvOxWX z35ped6y`fr$FB;QCM><{^LQZ3H;bkCxlFuj60%9{aq`HO=cob5=`Ct|ZNBZ4!y);@ z0^TI;YtgOjNw>k}c9Cih15y=|$c%}-!ENNbO$iLs7z#SeR8@_pn6CW6!&UY(cmlt; zp9t2KiBW{8%ESnQ8;}lg1&(ucH98CjU7tA`!RxF-j=JwyX|f(YumBDjyE2LUhmB9m z?30;%8EhbB{(4}+tebZ*uVHHKKgHipF z>4*4H9SeJGU~Rd{o7lvCqVFO(&?zd8C5eV>r0p#11cr#iBm59B>>2Qd6aNu?vaIFZ z@5Rg4>0w~B><#0Z$c&?aTeI_1e3AOBo><1w>I+L}!RrYEBiv1H)#qB9+ zcqc{ps9KAQ#f*aZFO>WAv(n?z-ZwY)xHd!+7h?GsDQ*Hp4lRhXD3`wIIWu-S;Qj3c zWf(nk_Pu7WT?WEu>~FGNS^9cN$faab)y$P~@;jk1I%s@&$+BOYv}6rViK<+Heb9Iy z3;HeS(iF}tweDaCFBF`sE0oEFHBz_F^gSY9#EbvN1Wqu z>g8BnqIdqqC69ke1?cdn9i1pMoYuQM$%09hybe<-xFmUWn+{@e4&1N4Ds$Qaq-;DY zWP!}=I2={s&duniN?gr%-)>A*TSS|k9!Nw2Vftm^9o)s{7l-sM15K8-cc`8$`&G;D zRMR0#Hu`NLB8AgD*M@~iK%y~uaXZqp;{LVVYtQ7G3=}^`70xZ}6^CV*&|z#0qQhxn z;T_(x-k3LOuqX@yfCd(!Lt3NC&PoV#CC;2>=Ad$=Y#cwmsvI!{sN|fU5~0};Std=- z9SeymOWIu|i6^3v7p%IZcRUcjT(hY?UA~E%9)~-!s9G=(EcdDfOD-*7m-)A`1=ewT z-7r!ACT3u27@UbJoyJNpe%qwtEZbV@KRO#yqWUV=L9#e6#GQ}|N0hCT zDb>zXt>u;$-8Mp}aB0*jg3T43pxZ}By#2@9WWLIS@y`x`!wR_f-rv@cWkF0PMRU{c zqB&)|1w-vu|Iv=jzKUpa%@5LvnjRI{GW=kH`OrOZ`I5A9fwHzPTc32EqO!Dt0TjJ! zF({u-mo0Luk^*zi;9%_H{{89sbY)3-;9c<8e z4U4yTmE@z@ve~-Xt{J3q#jb=C0A`{_Q&U!$|Dh_LOI0EWNPFhSQ6a=(TT^4<<$Tiw z?!*cym1-!zZ4#yqGA`IyZ&BQX5=51BCu{}|*A*(P)Fp@u-YwIoPmQyczRNhDXA56N z(z|xtBrz5m3!;n0b}+d%Kkw+Wzn*nXqRKe6;UnO1;8mU^zbInOP&nVo1Qdm1L`UJb>=1qt#(K%pd+Q*HsLZ^An# z`*&?Bz`n7Xbviw$8Z7QxsI7^I*iLim!_J%q50u+;58>LUL|i|&@DP=#YE6?=%GUG< z`7CzjBq0G@OI0iU8YyK?1Dn!t+W@j@8mz(YM2tGX7Aa4)S| zhA+L$nvX8u(x0CaMM@l@ zGYwFbz|(x79#{nnTY6xMg@r$Ql&C7m6yf#)!wmSByULw6P*1e*>|4oY@mW*HEvYB` zx0T+4Zfm+bj9Hx}oT*+vp>`1TIF@6O_Z)ZJE5+(gFkFW>)avtGk_F_SukA0ItIzQy zM6+8(NcT$Bf;Zb9_rX&H-i2RP<+2rR;60o4^Z9c^0{L9{3w3s3rU;P`_o+yJ(pPv8Jbp3O}f07%1NGg zypG+rEz3WDcJSc4P|Y9o*@M5i8M5eLfo{`Ir^2M~uEt9Hi#KcmDmS_EzI&SIh5Wi| z4l8Uo>sq$gSE!cHhKC%-*N2CIkAf303NtQ$m*$(&6K&+^b!|LsKUq0B@$;7=mhN>h zHfJr5?7G`Y7+Wgut_wO;=CYNqRStmBr}4-E)JzdTj>NwnnGgftfBl=VY`t7cT6XOg z{@3Qi79n?Q6N3s9>%AMik0kxW+iY8=JM7ziZZM)@?RuHpINY zlG)u201cRH_EkjyL)ci^7W(Ig^UbC#``jl|ad#~XpoV(p7hkFyh=nzncr}AZ!G;?D z<|+W1e6?<^K}_$J_YYQywI%|f6!0pw6$+pTdqOQzPI^3$^g=;*4r7nk7s4a269%j+ zQ!A?8S1w*0`DKkHEwm-Rxc+YJz4!NG?-R~WAD?#DhN{2Tq=UgJKlH-HhpA?@Ob8n02?voC_m_;_J2gtPu zV&+3xJcorn=wiyY$~z(xc~BBR2N=d~*8?PB|NL%ss~}sRYJb=M-Ujmf)faG|=ADj~ zzgtHEGmc{cD5#?eF;Pus!FxkqUNYtIm-g4rRNcC2_yNyQ^sV$aQ^rJk#mIz80wBC5 zV7mSMqBUB{cd1oP5h>QOO;rYg->K<*fbwiJAt*zC>l(WQDZl_ANCVSy%$zC=j!C~0 zUysLV+YIjl9%B+|39zkI0s^Z1EMo5UhtYdJ#*GJf(dzGSAlH|8ko6vbe4_J#O0HNV zQ?`G6^D`sUhV6$oW$5o%Yke_5iU4$7FMwdikJdoH;Ixk`$MEFSTwF1NUjIGzKP8gaIZ&*>)oveT{Z94f%t1P}<~OulP-CHzW+tp9d09c8&(nLj%WT-WT|KYdbL^FbB%q zVpPyO+v;g-?%E1zh=o-g+7t^~o8rgaS5p*!zie;1q%0>}P3ze$Uwk26{l(qY`zZOy HFIWBx(4vv% literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-5-add-alias.png b/docs/reference/images/sql/client-apps/squirell-5-add-alias.png new file mode 100644 index 0000000000000000000000000000000000000000..d5587060d2eaaf3db4931c0d94f209032efea3c9 GIT binary patch literal 22199 zcmeFZcT|&E_dkl`IJQwlMN|Y71VjWzRJt%0dQ)1!kVFMUgb-Q+p^g<1=>$XsMrk6D zNPq;A03$_8kVp+71f-V~YUqLcfW9;Hy?5R9yZ4WK*SdGTS&Q}XJUP!fd+)Q$XP>iA zB5qh1?%Q*GkC2eiK4YWnRzgBQk%fe|&FuOGcmg-_69q2Ye60+x3SnDLOaM1OyIwKB zA|zB4vv=*z4&Z*bmyv_7kkI~`&A)B+i1*GyLUv}x*RR+FK)Drrdr#Y<$MiNj4_tNr zB6m~l;`=i`bNi#C)s3bJ5vkXsj$TnM&aX7kUk<9FemLo`()|3u@H@w!Z>f>wMS=wd?vFZwhliWbkq=^=u)XQ<_t&sKrHaM!z3D&pOw_7UUn& ze}TV1-egBesAp#5z|~!+o*ck86zrlzyRB~r{s$gN4572{8BeQmkn_4*51#nF9K)oC z$<;HTbFcTdu4*kSZM}Cja3=UX%7mSo9wT?kv*VB>_crAaQ)3yv^{py#X6-kWk!2`5 z%l~HgGr#S2eol>@`ryJ(CRb>x1C^M7AA}-pF)X|4{CD~I{7e$^o#KLoDHa1}l!qT)&V-^S zN8bK;^WFaU7H2}=A1s&^H^l|qOPjXPA-NkOx!k>HNj_88&yiHuJY+#TK-mj-Ds(6f zEfqSCen_BWTiwpieYTHmgA3!50t$2FH!vS&UxgnCju;bW+E)j^t7g6i}iszgs zqP_Bm(RWpX_;~dnguS23+x|lM){H3ZFye4n$4#ijfGDkmc%e3vJeW6_f0nF80!LyX zL3^(S|W1@yYwxqh!vLqZORQ7uNrxu3uXrGnRsN$V{7l zqi1jxwy*kj)x#@Ooxq#r^t-|n!|1Uz!>wsC%AI!x83fcRBK<&9tkVAGKZ&Z(b9+`% z^6`)gn0n>YE0sd_&yrR%N%{-dQM)%OY&bp9i0!R$H}tPluhSj^ALE3O4>e+c-JQ?r z0R)D)GafT}1pO=CbZY=PV@uRXau;VD>#z8@k0`zof6JADd3y%~5ghr2@qEo?FXR1M z=icSlyOz0{Mbq`t@plb!@6IS6o0-%rtuxKlP{$YG@V7 zKl0165~*J1UvE{uy#mOxo_!Io zMrMSJ6&{sz;R=_~d-eh#!qQa2% z{DHkvVK9xUZu%2i+D!Cxn#voA7QH~oQm-4rmwjl{sACys0OP`z&q0$RWMoxV5(G7J zZy+slTtJ#l{_Pf(msH$;B-DGgbYM;@2wXNthV8{&Jmm0kPG&#N=z!rJUI}r zx-dsk=p#{&O|-WfN^zAF8t{n%H9TNui`*B87yj5?3|4*=2U_J8vecd?b)av#!0_pI z$;Gc;E$7{9$S{Jx2CtJQ3}g9u1oT)i!>#*#Z)?nPRZw)C7395Bgy#Ioptz~d2<5f2 zaK!=XerRnQ523z|Qz}k{oO0^PDN(ohd_+{FO75Xz-PZ)#wVF`H;Zok~Ym3FJ?G&j_ zcb)foJbvk2ov8X}2d?HQ&7#>DW=URghe(Q`u)nWMv@gdcAxEA(OYh79qc*rZ)t}(r z5OsrBo~Vb*z}B*+`~3x$17Vb5w}Dic7O_Ed0N>t!LizHD#K0@ME)!DK{OobHaz1MJ zPPkVR-wOKG{bz-w$0H{06K0gELLxZJk!XA|(kO^}mW))jSNznTyh1Nnx#N7lpE{R) zuWGg4IWX+N$tSUHrUOQv<>skcR1wPuzR$6ziJTAPlNo!WH~zf7CPALPbYPa{DnQ&l zgJ0BO2!QadP3Lq85yUTNdk6~d z@vP9E3IbpaN2Hc}7n|Tc9S&Ig@-MQa_;d~rQX4Ascqu1TV7~?-&qyWRUrYGJa9P~h zUwLY6ZuCO6=9Ht@{*6dh4Kssi8qgl9pI+*&u{0sPS0!*sP?Nvm7c-p_Ls1GI+>Y2& zUo}Bvp)Q^k!wQv@bqfz)QC^7)D2Zi3Pz=QNq-WH?kP?~rRwJi#PUgnMDhUG9$JuXb z>1B($)K1aAjJ$?coD7l;SGCSPgs=PtFjL?0Ml4+NVnQhg+ApB?YzSJlpFYl@IB86u z*M3IluEt(P`R~V}M{BCvL&KQUZ?CPN4W7*+KFi0JeTBQ8$&P(~{a0$ez2+I!1CMsQ z&^>8NzCGMd>reOPW#x<)h&3p1iQ+L`-I=wrp>HS1_I=q>PIbTawA>Kiy>eSMbmPUZ zh5dzsvu&RITg14#f;;cc@_j!%kOF8m1FvKtodyfZj&AK4n65F62HsId#d`bLv&a}N zjo_uyup@nCqcPRu>(oQl{X9YVKowUIJG~Mjb);`ajQr$UIx$FNyhwOAZY_aTEe<2Z zFfHIF3W^q<6%EA1Dihrk4iVR#?L7-lyS-86CLAi1=c3{vU*7#gbL(37Ce5=;sr2hT z_fG|*$k}WABr>8y#xeUQe^n)g^_|C$*+zzVm^k<^9Tje4=b|qg8osVL2{>cCO`LlK z$g@4C#YbNsB+2!~@$Ra2;)8}{U7r;9O~3J4duaJ zhT&5XmZ5o0hc9wx!*J>-bh&_4{Y-tWlxgAgZ1_&^(_Uqo0(oeMpELslm?oeTRt?W$ zEWyV*N)CKV!fU@5E7dfXy66JC9_Zc@-b3J`JMT)>XM1XUqDwEsqAv}#90S~Qo|Q&C zc#FE)1g2xPHb7aYTliCJ=uETM{9e>|DLcS z=3^=eq4#m{qzys7D# znK5?EV_(i)k%B{)-R+xteqNto@s^b#`(2;6fkm@UN1iEX&e%sf4z9@@BK2izcAUx@pYygMd~zuc%)5 z0b7htIRD5cJYpf=L3;6(ANZ&F7r;C>G4TcQ|6`++!ruSImHKM~ zZ}j8G2N=$EJ@>Bu!`0^On%B1Ok8}zr$LA7z54314eVZoHE3g?(Dy!azl$scoz4em8 z9``@s-*`aJME6&Z_Px0q&H8#CVq81+2iNHlJv47|^;z-CSo^1)8Mbd+cH2MmwQR{9 zXm3O-sj1kE_14yF(1U`kCRf^XG8Ex3A)$MZkHxV@NY9^hZasWIy!fp+e!Q_w8eeqp z)VYF)nD&c~e}u0Fw69#c)A^eJ_H~--!!y|(yFAZ_F%G9&$ezWUD3nx483Z-ByNr>G z+o1iV^^%FP#I$@NA?u&;9tUSZsJ8gbP&>#$gIJn^ukg%S4-=-E@p+6MvJaeHZ86pE^%=DT1 zM_yzn3^sVaRY;Zy@_PS^fBuMT8q#CAzm>i?sux5qY_Bn~P-yPtiag%<9L@aD^L2v< z?Y)hUi#EFPI@$@%d@Nai>fy0O;TIK7P;26a8O1u|cCHd@%Eq4hO9r^KmOtn@-XRjmR~GLzX$^3bRFF&CdA{ZKjM|)r4vM*Aa zD}SjW+H}K38?;8M5944RC@I%eL8#h^$6rt|!{a-I{y5V7oU>o~M*O7oh@*x_T+1$`?w!L2KiSCuItpGqG|Upzt9&CyxMvEyGm z@w(EU3@oq_PS01RRK#~#FrJa2wzz%-Vy z6jW7~ls@0cJ8E>#yEhQmIQq1&=!6zjDF)Ci<>Mh~FSC-aPI%-8ROV>Z^F#VK`d^;L z!EjTa9(w%p=1JFBrK!@)u_Bfjpy0J@QJnoz@8?x#b|@c=Jt!ioxF3Z+cj8`0*Rjq{ zn?aG%&V2Rd_sl=m>naTu4hWpAd{0mv;tpJnuBoRJKtatFq(i3!cy9jD-TMT@xwd3G znI5MeBfQ?_K%DpTEIm!E5B)7&d~{7AS$MApR~RfaCvo%PMd`dT<}{im5Sba+~r$YoGgfcNm?sxX}i!LYR!3^{jdY_^&$^X z#oa#=E(Jq*D4bsDRy-jRw+E)>3=xr%c+QSKkvd)t{v4eZr5!zU|KqtN5p|j4`|5+= z^R9R6e(spBI^B~|*W?*fH$@XdhwQWh>s3#t?W}PbU2`yDa3$zp>dsF{2`hl@`v<}f zGzYh{hu^qvzjBdyb6%GCUb`**V(}}w3Xj~@OWQ;C?2VpR|Dw4==r=@j1}BMT9aV|G zdwV@7;El#}zY{@r8u<;s>wHi%F!UHA{PJs~56HPziPS9&vv-fqgy1LvF_phOjw8?1 z2jB2N=pp6A!5bZIigP0)9=l+syIuNxKlLxvuKEJBUBY_0zK1eNb9evnm;AMfvus`3 zsb{IZ_D5IR>Cp72!n4y?5-+{QGQewKG^&nQ|!o*HRdb1P=Ub;_9d_8{^~yD zgB{0&gg&1S$AfP`HJZ=d+nDUem>gf(DAbYOF;x3DJ|%8d(RBGYZW1*Ui7=Xj6sXSH z`WGSAakiLO(hG`IlZPEn?#eBF=W+SW6q4`n&yyJNBk!z<9vqeTe4@)}0>{$Ng5g`E ztwNdm>$3X0x%!ez!F|!%u4L{pT2!nQ@13Cd=2P|6OHJoemYnto-EK+Q#@NyDyMoNitWQ2S<-Z6CeHC>U`|Irq^ojC{ z?YHxOM{N_bddT@H@fB0G>9-Gisk;GJ9lOUFP;S5_9{F)i%8O$aw`fIfErP^a-1*mI zGLMv>5j0gne7TuI0RiE3OGgv0kl1%znashzokh3QiNVb5ZcymYn_aPGgL|$TnNt>j zJ(YI>gI5%3)kp0V`4lfD)G)h~A*<$drHV&l?>coe7J|1)xy6vJxq<>&6mTSw9XAJD z^;Q{Q-StNm8qy)@nK++3NzqFHKo}7n) z#O?8PI{GsMqG_x|c1=g$>vrYp-pL5*N{U0{pw|2K)}}wAxIK1pB&fJE9)N92S5T=5 zRwzr&k;BAb%~}7jMgP*7$=n8;ChU_SRQO{K_DxnIr2Odf@nD3baxn@|%nA9k6G|D^%`Wv2h@S1LDnnQ)sg*q2x6~VWi0qp_CEGP$O1?6*W}addXUW44-1KNZx6j-jnPk%hyCtO}PSS zh*>ut>7MHX-z3Hl16lrdDCP205BazZ@wY@gScf3^*zx?YTh0Qlo(5i5A-MW{e&nJ} zDVasL%InTVc+ED-htJXU!cJT9 zNvHSvqz1r#hOjM*8jWU%}B15KcGgW2mbpk?hYOJv(ocN7hu=Piiasm{sh(d}>Y-jR=@JCN2U#_Eu-H z*Vi;6!7yd&tjBRi4(6-ejE7?O>$1vbHi0u&CXAd=#RpaImVfz@(=>C?A@PlWA6hU5 z1)Gbcu>2awv`pcUEaCKy&n|@EqH1lxSdRJAc<1+t4a^QQr9?mcY_HN#Um*VSNFmc6 zrrTrTH^WWF?QvIGd2ne@S1K{{hCoEMxS8F5J7_)h6nWXFQ8u}3|66(IE+5-d z*P!LKV7aNNCBdM6t=$f~?&4&$>A94%Zx7=1;8`|9ou%CR5mcXPY8(Vm^T=M(Ns-;_ zOZ9qC-KGW{y{jziAS7Z>fzD?cGQn(2-|rGM2(DU+*H&J*`XE!xslo^&MnQ_NiV@fA zOg$ft!xGbGGoTm!K6UOLK%z(7@1`I{r*$^zMY~-JT%DLdp zCc9^iU5FV%9A^V?{}i#PlZqR7KVDcYb|npMMmB( zVkp8gafLzs4Wv5Pny-b%fHvG!+Nklp2}$Wt>1Sq6eVP=A@fkcZyy>V0Y`FS zth=#Hi#eB|>L7>oD3?!O5NqZ5#z{u#>dn+1s$sZhT(-$I*O_?fv?@DC2(_k=dU8OL z$#uu%$@qTh(!RnyXGzE7AZRlMvQJ>@8+PPRh>I6_bqi zDf?X;@fmPGn;gl#VZdyyWfp>u&=tqN=$9)ucvc2fgXKx_#?GKR!U-2lJ#_dQ{SYLA zo?}jDp6-d}>`%jG-(ILCj-J42SJ`@Hw|qwC>>CTQ^)sU=-5t%o<0)c@AA2La4^rgsTq zwzmsm;z%dBQjrdhM_dmwFivf!$v%k9xwbFrY$)8f0PQ2(EW*^2;viAyQr#J%dG|?C zpZeNe55fNY3~4y-LQiy`RQ>}wGJ`LJY9f>ykXKN1pZ6dV#!W~~Sd3K~J%G~cdk+<{9Kp?*@ zpAD>}nn@~B%-<=-EgGzaiWG^SFWwMUSxmh(Aj-g~Iw-52Zlfji>RpwM>)3%Yk9dCQJHkyj*>YG3N|$kbZbMhk-S)Q~pSw`$mL=@W`m>`M;pM+C12DKN(?*;hd>*bczbQqZDWiNW~4~rFYE|a8*R?6?>WAY48 zL+cH}j#UbTsDR6?XF3QbPlJIO>aYd&8D$8kpUl4`Je0gL!O~_xma`-bwv5q&6SY3F|w= zTlbkupYF0ncj9L+15WLd%f0?*Ot`BryIY?Rt3_b1_leJ}>StEHJQ9h${zNH> zMK~zWEP1CuW3RN9G)EFxB^Ie#uj_y?@lNLhKvnb~^>>_!^WE7Pmf9kx1suB-A((Qr z58wNMDPqq?D^}Fb60^r&3@=E}X$5iw$L_2KSK-}r|5jv(Gh=YVERWuiJ;s;KW73K3>K+;l` z#}S$i<#F`pEz%vD$5LLTf6l+aHAV_vJ*&;EK+bf*6aELHYn%s`8ES>~DC*&CLy}n` z=Y@@zc+JTMUej*8>E@!u+DY=4ft5IA0KFUpfME2lBMzD~P#M*XxV>7ZYGW4DPn|~Q z*nMBK*mX?o@1=?y0~Aa!3EJAFIMAk5ZPUnG9?PgYEi(96-{?bkhFZnQCYW^yPRvVS z_1ZH+c(ntc>v#i64>e% zT24`5K=`0sxtJ1mBde~I-UPG5X!JCd1*mLXf=#(h<`pO-v4)u}{BIDndrUjuz4 zXonrq^{tB($lx`t)R-#JN9-m z3|ocxL}W%$xqRu~0d&O(E$H*r0A9FyILnN21AG$_V$pW`NEfIg$a<-sjZhj{VD^(` z)`oNalVvDlVWaK?lWb3AD!WTUY>VUxfaJC_SnI|2QRG36hfGRNn?yLL||ouifwpoyP1RnlGy z8092+?$DOp)vln#A8}eNum4WISHzayQu3`w@o%mGS)|bHV?c5isNKduf+-|)QU%~# zhaswY>a@$xA%{$2Awoib_?;y0n{fsSA32pL-HqOAwke<_QxtBE$6^e^W6mLd){fbf zT-J65yuqi5NA%pF4+B}?z5hIS!aDme*6W@+Q8_R_yS&{$z-vi(vD{K)zJV{pzeqhr zHk3df#*=H~oOqbrrm}?{Kl%gS0}b0r>Y*ks4HO0G$vX1CXjdZ;tJ=U@4ZQoCiJ(_^ zPe`vzI}iU~B|4Y<(_63d;Wgdb(G+>}0Q3&AyRxmt>hu<|ke$G6ZSdQ1#&T zXHw4$xij5fpY$BX*6B(c^1X|dRU=)PljKcUS32;bW?^@oUf~l^ZJ9DuFk&Lk5Wtcv z@0|AUT^X_R+YqRO#hh;)j!jhU{W5kq)f)l`RuK+ z-dHN%BKf49A41a??#@s;n_S=GGn(R>#6`2u8TK^sRHg(-y{hFeRpF+V0oTjsuf?1J zLW^u4$$M1{8gNIUT>g#M)umDODhJ6!XAcb^OG{v)boll2J@hEdG(@}_EnG;@rZ5#0}qQ-4$MKgwzK9i?d zBOp|TZS=A9R`a(PuRnTi#8VV4b`&(_w;-WTKP4$We&-iiZo88RqjQfWYJagCG# z+gRzZsgLO{YQ}{HTWH_{bfzwAlh~iR2nJrZH+b&LzycE?478$C1;l@dd@=4xnO&*m zgf5R3Hcl}r247sLxf5QfB=bm7`n4$vNKG`Ii57m@z4=mxE+CPa80ulnH!M((0?v^q zo+ZVD0}pZv^JknF9m1hU)91P(tHaQ%pQmB{r3#d9)0R&KF=eMuc+F?u;d*E6Q`z`y zth#e!Ez|2WQ|f3aaxQLUNVv+MYor}tCQu2Ti5%#0$L;H1Zt*u65asxMHzr?M^4ap0 zCCpTRni;Ba7+9gOeE~T~j`t>BxCqp9xBx{eU$*U(_=fG*M~Nwt4vI44;#lv^Vtqs) zQS|g=dldY-AMVu+)W$+QxaL%9*nvLOa!mD2P@kmk(#VEHOBqjo;9Xedh2RA+xTdpz zRd0YK)hn?+w&4%f;C~4_z$;%=AGjb!3CfZ%?p^N$!6Yg-1XJNtAyYvknYaJ38y3Gc zX0H@$zRn7T6MUjoi^vCG)R}3t^kj){Xl4_^RRaM@Bn|QhgC}Hv5mfMGj>J}({ z5BeLs-JYz>U3tV%FvI*5DL&vZ(ajkYr*$ZbXW#~KWQn52v50&FRR8kgz|-&$RZiHj z?7;8G7$H-xHHx~^<XlJ@j6VH;jZvgfp zcrX*8A{91EeNbsO<2cE>r4#qYzsp;OYc#l!vC@(IY)p5td|)`iQhAE?Z`IP=aQwIn zi-AS}g>TrGg8=5E5G(J!;k*cV&_$_`FRRN`${0?Eh%_tixv6rFnAltQ0_&^*WDzif zL!U5)U$PcC>AoCm6GlaGY){4!&Djw7r^S{*bR%6(3| z1b=>HV`KKvLKlju0>VF*IH{5|vgZ5AE>Pc#pR@yUm~NWD=}E*h53zDu9aFU!m)Qlr zio2DrFCGy=mJ6ff-YGh~h=Wvwe0ov1>!Zn37Cxv+#By(Lda!d`X47*{A3uLkC(`G^ z&8g9i5X+R9^w6~}Gs{>TmLC{So;s+z(zm^)N32j`k*RFB8SL85gYe{zFy#Ov)MDdF za%%$?WchUriC1L57|uBiiFnQh7LYU(J7mZQ;~4f2D)6KLJb68hcFqvBQF!21<8G-y zL>V}JLn|?6C0IaCXC$-QEmpr7X&=N1Vx-m@ylBK|MVW4wzoS6%3T;Q^UlW4zAy3#> zAgL|4z4FAy+MoSMns%9i(PM#6&??6t1bJ5&OW#!-{1CSc41L`40rNSwHla1MpMX#x zSDkrcH`_O)*9`=u4-G4+mbZDpv5cKzG4fti$m%A?h z@g*1*LFC1dt!^M*k$T;ee{)zJo1ofvANM5-Pkt4k8rTh9&q59&hXS~WfPwu$b~L>Q zb!-c+MwiF4=oLfnXDeBE1?x^N%%gI~$_&h|nFT)w&&JO0u>XswrGa%0R&e16O6gG5 zcc8o(`bLslD+7jNOg~}t&K3<6MAlHD$md{?syncBOyRcp0>I(YjQbYS%G!+YhTa=P zf-sff3Fd~CE*)`yS~YUBC7kBI#Sp^<>%6$QF}-bMNJ?*tR?{k%3d}QZV!a(hf;Xnhq{=Ghp z=Nhs4qeH6pl3RRInzK2flhpoQwBUsd3`|=tlMIz;ENJjF^InT(iT<=jX>xfSD+&aa zLWRr^H8GD2tK^BopTs}{&6mYGDJ$^~7|4|c*iY20t=rh-@>qySZ@BK@%@L{ivb79P zCF*Mfze^3u!@qnd#jyeVyl`z>gnz|GO#}9=M)a;J)vbhJ*hSeHG`k2}8T&|o?fMK( zSmsV^GXjA0C&{Qz&5$CGs56YuBJq;PV*4xD@FZLo{vGdl*21 zM0IS!esO`3>y(mDl4HfcqfB2FviTL%DfKNmL$~nif4%~cWh|s&-xjxGK-Tm(wR`c* z1I}VT-|5@IfbYWZE&mQ-f!yh5#?#)d3=R!|Th%Xk@Ch0Gqs)JvaPi{Rh0Z&20^w)B zlMGYLEN;@{P2GJZS3HuFMv$269N#n_$r+YT`E) zo4T0G5l&COLUeb;4Hb`O-vJ5+rQ8+^|4mk|7dyT#ep`zpxywBM^m5(>zo2lqQ`zOJjT+*MTD{vwDO*N5 z3<$fSxzTe-P>%Vy|GH{??H83O1MMNLn*uvFvzE>Dk%`yW-tLWMJnJ~mryo9;&y#K&-2qk?HWS1Dy;c2V?pwC=|CI6m zzdj5QFLg)H$Ly|`3JYEi4wpdfS}do9RErp%IB#Y7_#YpK;k@W=8p_oZOnjt6gPw{b zDCa%MT*78n_m4wugw{%7{&4wWOOB1$=RC2hrMgN^pI^Iq z?`I+E5A`UIhhQ8-D9fY3ZjZg)52uge@XE*1T_oaIV9#<{kM57Bbe0<}QEXt<;D`TC z2HtD5PM=Nmxd_4oLMuo9OIE#&p8t*rvSg+w86NOyFZ4j=N+*s)1@Q;v09b)Ox|xr!^2msd)MJ$Y(TRFrfRo1B zbWP1;$@xEhoI&Z-`#2EjAB_W4!IQ{B4g9fv|F?9A{rMk;rO4nO|7QdsGH#f>?#&Pn zuln z_#SOunUiL`;v$b%(rz(;rz-ohAN;GwSV*3A!6Dtb6xx$iM5Si04^?Skma3vcRWAGX zlfMK|6YwKGs*2&sHT$)nYyxtb5orqkoHG4iaydozVLo~}`gugPiTW?q0r;#$eo{=n zsC4b`NIhzRVplgPpOgO{5=XD12e-eiO5y=0~9L84o0;W>=$K)k)Mgco1|L!(b z58=s&MPeaF-yuwLUNS2iu)vNVx)jJLjgQrCP|o@e!2nPLAe{f+{8tA5)r0@qf>6VM zW$<4aY-Y{>)r0@LGRQIB+Axln3yiXjWxamSIT){cH8`Dt-mHPoig?87`uU{T8&@FR zGp+B~gfGChE(q9B=H7p0Uev2ZjMIWrNL#g#&M4r#2l$-35|-eZy7K+e3Q*ezPK-H? zJ(LT-yxHRcRdJwT9s|4ay?!#wG{*#0f%B2xYFW!jKiU$lRFXtcH+Bxgh?9ZpR13G=lq_6b$v?995e;} zaXx2xpN3mMH&F4;hHH_qIk!`(eYS2v)_+yCabpT*L1r6>O#5a>)`+u z)Nx|rn-I4LLA^zjs-1B8vC4@KlsKtsXp%XCq*KDX<6-AAuzX9%E6Tcl5j#vE_+x@GLMOKca7)PB3(ku^~CL}v+Ccv z0i%tj`z&N*xoY(vs3mXei@y-$S^d4Mw)FKZ<`-5!*7{o5$$iE0WYvj?%$U9U!}~m_ zL+T|hY}KgddTu}hWNco#Czr3!QFT*PUYvZ8k~UcB=Fm$lOQ$E9d0$SUFE~%O`J+4V z#Klo$n@_~^XT(anSPDh=zSI!CrmLO)JKJdTv|&;s%DF9c{Q1Mv&?;V8gEN(jKFR)! zM9xNso88J8A@$E>!P%;;od`%o5}mKDYf;W5CJ-`d+&~D?DW=MTRH-|u4m4!D8<~8qgD_~ zql{M%SkBA_NTg8UJ&jBYN_2}mM0+B*HyYrx)|wLC!EbVb%7Jsn3@G~|(!+bwbtY9; zm!%MfRLX)!2)ZPJm6YaDe%aMSe5fT`G_>N;MPSjY>9l-gdh9|}7IGx3%?F)`PRXjY zxTWVQpPm&Bxkq%8XGIfotsxFN4Bdv*CafzFF~(kzPGF^X<>&Wz_@{6J7s_>dDmx3O z__FDPy4@%?A=71cyi_59o6r%5Rs!lJokN(0X<3eopV*HHV}**=lxBA-K_;fpIoUX< zkAw;vUTa;My}!yeFK5yfMo=DLR2#oy!6lylJ9VUJ;zSg)C_%a1mdqf12NJGvxd5~w+{B%P*jG6I6cU8Jn(eQ}{I3;j~P=Zb4%qqi) zg|5Kf#t0HlFH}q*MmN(j)I@eq)*KSf(DD|LP=h2SwbYR?gkho)AM%~3r6Ey2NJq2O z2KhtW?h&ZeAgY3UvE$*U7EMN|@=mvptsGUM=V2Y?5GhHGnQx|N?^C&1h&|e9ml+ZP z4)zR{J|~YzRgF?4a+yBbjL@DD!ajhzi4`P23y_3kKe)qy~a5Q-#wB^z`Dy%4VNUGD`wvhK?L3MBr3Q_-}FxDC{Ey}F0k0Ci_hA} z@MgI=AyG{DBsq~m;F4yKN~Hz}5MpkVkro7D0w6?fLUda4*n&nw*4*qF>JUF0o)SYX zwN!0%f=F@D_lPRAilIBqHk3yYVP6{Eg(R%nDRDsov@O|Zs$0cnRcpdK$*qVP!EQPY zH%SX{fRJb3ZxRMbyit{0+0ibQlL}X1fZ-zCwh3e zw8@25cT+iLx^4_{07Lb4YgbNg^vvZ2pt%j1X@f|=w%LI4%Pk8>AEeZ>aXLLigN0SK zbj7V0@g_jc=iPOAGfniIRlmipoVb;lz?s81r?B$|eZ9AoX{EhgboB(#z1#rpTSpen z`>qqxmFaxs3uZzfp+Y$%*_f>^cvPL;O2JO?_2qEFgcz+lc74?wXL>71tI=2Rt<_14 zDBqgLD|^X)oY=S?$fRGC5LU=%?j$!?t}ncDOQ~H@gvzm1kgKTja|wU|O_x}2g6`Mtu;Ppu3j=XM-|eaG(n)?%V^NB5(`I=oKlQD4x7l(9rs!7 zc~B1NS8flbo))yL;<(1Ew_!WA>$r7rw-h4Q0y*Xl*uR8!9dn^4B9|00-_tg`gr)!* z7y|DBDkdyYP&O418S3InWUcDNf-DLK51DejHLxkId8+3Xq{(mP3BlcssnL1N;gH{z z=p8vKvsz;u1aBcF$EnF%a{5?Zt&R4T&J~oc2WDwGt1aKFLPl*5Uc2U|2I`4fIt#XuJq^NX z;E`FC^yo*Nct?jue7>~%h+PC#P6P_jQgkzxbu7BNNM`Ppb);{VIRyzv=`BSL$l|o* z*4opmj*I8X^D9t^ZWtERbM=nK+Dn8jm%;}R{FNVOp9Gt0gr+q1WfUYunKDLKs|K*X zykoZLl)FLUVFJRZK-dPfo0+(eZ{DLdv??G8kl6JWBe1P@9V*a8pd}ZSF;n671^yV0 z8!qr;*y>F1lT0ZD${EM0=TShJn%CiIiIn<)yX-nJ{oW!SD!D&^>z&=azLu=dTee-& z7eTKrtAs&xOq|#01Aoyg)j2b-sjMwptseAmqmO6PMI^jS^Vi7*IQ@sc70g)1L@t@>h5p)QCsSG}!w zVeoIF5=lLaP!bEap%OWx`!hEMZk-vTHcS?}S^Hk!9D z

}gL!GJ#h7IreU$SGxR0S=`Br1KB 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∋Qet@idqx$8k) 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 Nx~le#yz3T${|ieXV_pCN literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-4-data.png b/docs/reference/images/sql/client-apps/workbench-4-data.png new file mode 100644 index 0000000000000000000000000000000000000000..602f09d06e46ff252cba0b24adad1f9fe7bb21de GIT binary patch literal 75863 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 + +==== New Connection + +Create a new connection either through the menu *File* > *New* > *Database Connection* menu or directly through the *Database Connection* panel. + +image:images/sql/client-apps/dbeaver-1-new-conn.png[] + +==== Select {es} type +Select the {es} type from the available connection types: + +image:images/sql/client-apps/dbeaver-2-conn-es.png[] + +==== Specify the {es} cluster information + +Configure the {es-sql} connection appropriately: + +image:images/sql/client-apps/dbeaver-3-conn-props.png[] + +==== Verify the driver version + +Make sure the correct JDBC driver version is used by using the *Edit Driver Settings* button: + +image:images/sql/client-apps/dbeaver-4-driver-ver.png[] + +DBeaver is aware of the {es} JDBC maven repository so simply *Download/Update* the artifact or add a new one. As an alternative one can add a local file instead if the {es} Maven repository is not an option. + +When changing the driver, make sure to click on the *Find Class* button at the bottom - the Driver class should be picked out automatically however this provides a sanity check that the driver jar is properly found and it is not corrupt. + +==== Test connectivity + +Once the driver version and the settings are in place, use *Test Connection* to check that everything works. If things are okay, one should get a confirmation window with the version of the driver and that of {es-sql}: + +image:images/sql/client-apps/dbeaver-5-test-conn.png[] + +Click *Finish* and the new {es} connection appears in the *Database Connection* panel. + +DBeaver is now configured to talk to {es}. + +==== Connect to {es} + +Simply click on the {es} connection and start querying and exploring {es}: + +image:images/sql/client-apps/dbeaver-6-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc b/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc new file mode 100644 index 00000000000..dabee6430fa --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/dbvis.asciidoc @@ -0,0 +1,42 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-dbvis]] +=== DbVisualizer + +[quote, http://www.dbvis.com/] +____ +https://www.dbvis.com/[DbVisualizer] is a database management and analysis tool for all major databases. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC driver + +Add the {es} JDBC driver to DbVisualizer through *Tools* > *Driver Manager*: + +image:images/sql/client-apps/dbvis-1-driver-manager.png[] + +Create a new driver entry through *Driver* > *Create Driver* entry and add the JDBC driver in the files panel +through the buttons on the right. Once specify, the driver class and its version should be automatically picked up - one can force the refresh through the *Find driver in liste locations* button, the second from the bottom on the right hand side: + +image:images/sql/client-apps/dbvis-2-driver.png[] + +==== Create a new connection + +Once the {es} driver is in place, create a new connection: + +image:images/sql/client-apps/dbvis-3-new-conn.png[] + +One can use the wizard or add the settings all at once: + +image:images/sql/client-apps/dbvis-4-conn-props.png[] + +Press *Connect* and the driver version (as that of the cluster) should show up under *Connection Message*. + +==== Execute SQL queries + +The setup is done. DbVisualizer can be used to run queries against {es} and explore its content: + +image:images/sql/client-apps/dbvis-5-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/index.asciidoc b/docs/reference/sql/endpoints/client-apps/index.asciidoc new file mode 100644 index 00000000000..ee9891040d0 --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/index.asciidoc @@ -0,0 +1,21 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps]] +== SQL Client Applications + +Thanks to its <> interface, {es-sql} supports a broad range of applications. +This section lists, in alphabetical order, a number of them and their respective configuration - the list however is by no means comprehensive (feel free to https://www.elastic.co/blog/art-of-pull-request[submit a PR] to improve it): +as long as the app can use the {es-sql} driver, it can use {es-sql}. + +* <> +* <> +* <> +* <> + +NOTE: Each application has its own requirements and license; these are outside the scope of this documentation +which covers only the configuration aspect with {es-sql}. + +include::dbeaver.asciidoc[] +include::dbvis.asciidoc[] +include::squirrel.asciidoc[] +include::workbench.asciidoc[] diff --git a/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc b/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc new file mode 100644 index 00000000000..c5a30ab15c9 --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/squirrel.asciidoc @@ -0,0 +1,50 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-squirrel]] +=== SQquirelL SQL + +[quote, http://squirrel-sql.sourceforge.net/] +____ +http://squirrel-sql.sourceforge.net/[SQuirelL SQL] is a graphical, [multi-platform] Java program that will allow you to view the structure of a JDBC compliant database [...]. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC Driver + +To add the {es} JDBC driver, use *Windows* > *View Drivers* menu (or Ctrl+Shift+D shortcut): + +image:images/sql/client-apps/squirell-1-view-drivers.png[] + +This opens up the `Drivers` panel on the left. Click on the `+` sign to create a new driver: + +image:images/sql/client-apps/squirell-2-new-driver.png[] + +Select the *Extra Class Path* tab and *Add* the JDBC jar. *List Drivers* to have the `Class Name` filled-in +automatically and name the connection: + +image:images/sql/client-apps/squirell-3-add-driver.png[] + +The driver should now appear in the list: + +image:images/sql/client-apps/squirell-4-driver-list.png[] + +==== Add an alias for {es} + +Add a new connection or in SQuirelL terminology an _alias_ using the new driver. To do so, select the *Aliases* panel on the left and click the `+` sign: + +image:images/sql/client-apps/squirell-5-add-alias.png[] + +Name the new alias and select the `Elasticsearch` driver previously added: + +image:images/sql/client-apps/squirell-6-alias-props.png[] + +The setup is completed. Double check it by clicking on *Test Connection*. + +==== Execute SQL queries + +The connection should open automatically (if it has been created before simply click on *Connect* in the *Alias* panel). SQuirelL SQL can now issue SQL commands to {es}: + +image:images/sql/client-apps/squirell-7-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/client-apps/workbench.asciidoc b/docs/reference/sql/endpoints/client-apps/workbench.asciidoc new file mode 100644 index 00000000000..e50a086ab3b --- /dev/null +++ b/docs/reference/sql/endpoints/client-apps/workbench.asciidoc @@ -0,0 +1,40 @@ +[role="xpack"] +[testenv="platinum"] +[[sql-client-apps-workbench]] +=== SQL Workbench/J + +[quote, https://www.sql-workbench.eu/] +____ +https://www.sql-workbench.eu/[SQL Workbench/J] is a free, DBMS-independent, cross-platform SQL query tool. +____ + +==== Prerequisites + +* {es-sql} <> + +==== Add {es} JDBC driver + +Add the {es} JDBC driver to SQL Workbench/J through *Manage Drivers* either from the main windows in the *File* menu or from the *Connect* window: + +image:images/sql/client-apps/workbench-1-manage-drivers.png[] + +Add a new entry to the list through the blank page button in the upper left corner. Add the JDBC jar, provide a name and click on the magnifier button to have the driver *Classname* picked-up automatically: + +image:images/sql/client-apps/workbench-2-add-driver.png[] + +==== Create a new connection profile + +With the driver configured, create a new connection profile through *File* > *Connect Window* (or Alt+C shortcut): + +image:images/sql/client-apps/workbench-3-connection.png[] + +Select the previously configured driver and set the URL of your cluster using the JDBC syntax. +Verify the connection through the *Test* button - a confirmation window should appear that everything is properly configured. + +The setup is complete. + +==== Execute SQL queries + +SQL Workbench/J is ready to talk to {es} through SQL: click on the profile created to execute statements or explore the data: + +image:images/sql/client-apps/workbench-4-data.png[] \ No newline at end of file diff --git a/docs/reference/sql/endpoints/index.asciidoc b/docs/reference/sql/endpoints/index.asciidoc index dc4289aef92..59c397f97aa 100644 --- a/docs/reference/sql/endpoints/index.asciidoc +++ b/docs/reference/sql/endpoints/index.asciidoc @@ -2,3 +2,4 @@ include::rest.asciidoc[] include::translate.asciidoc[] include::cli.asciidoc[] include::jdbc.asciidoc[] +include::client-apps/index.asciidoc[] diff --git a/docs/reference/sql/endpoints/jdbc.asciidoc b/docs/reference/sql/endpoints/jdbc.asciidoc index 6a8793f7e24..a8a866ac93c 100644 --- a/docs/reference/sql/endpoints/jdbc.asciidoc +++ b/docs/reference/sql/endpoints/jdbc.asciidoc @@ -3,14 +3,20 @@ [[sql-jdbc]] == SQL JDBC -Elasticsearch's SQL jdbc driver is a rich, fully featured JDBC driver for Elasticsearch. +{es}'s SQL jdbc driver is a rich, fully featured JDBC driver for {es}. It is Type 4 driver, meaning it is a platform independent, stand-alone, Direct to Database, -pure Java driver that converts JDBC calls to Elasticsearch SQL. +pure Java driver that converts JDBC calls to {es-sql}. +[[sql-jdbc-installation]] [float] === Installation -The JDBC driver can be obtained either by downloading it from the https://www.elastic.co/downloads/jdbc-client[elastic.co] site or by using a http://maven.apache.org/[Maven]-compatible tool with the following dependency: +The JDBC driver can be obtained from: + +Dedicated page:: +https://www.elastic.co/downloads/jdbc-client[elastic.co] provides links, typically for manual downloads. +Maven dependency:: +http://maven.apache.org/[Maven]-compatible tools can retrieve it automatically as a dependency: ["source","xml",subs="attributes"] ---- diff --git a/docs/reference/sql/index.asciidoc b/docs/reference/sql/index.asciidoc index 33b9da9fab9..aa9eebea7b7 100644 --- a/docs/reference/sql/index.asciidoc +++ b/docs/reference/sql/index.asciidoc @@ -36,6 +36,8 @@ indices and return results in tabular format. SQL and print tabular results. <>:: A JDBC driver for {es}. +<>:: + Documentation for configuring various SQL/BI tools with {es-sql}. <>:: Overview of the {es-sql} language, such as supported data types, commands and syntax. diff --git a/docs/reference/sql/language/syntax/describe-table.asciidoc b/docs/reference/sql/language/syntax/describe-table.asciidoc index 66c1829c14f..ebefe9bc34b 100644 --- a/docs/reference/sql/language/syntax/describe-table.asciidoc +++ b/docs/reference/sql/language/syntax/describe-table.asciidoc @@ -6,9 +6,12 @@ .Synopsis [source, sql] ---- -DESCRIBE [table identifier<1>|[LIKE pattern<2>]] +DESCRIBE [table identifier<1> | [LIKE pattern<2>]] ---- +<1> single table identifier or double quoted es multi index +<2> SQL LIKE pattern + or [source, sql] @@ -16,6 +19,8 @@ or DESC [table identifier<1>|[LIKE pattern<2>]] ---- +<1> single table identifier or double quoted es multi index +<2> SQL LIKE pattern .Description From b3071133d4f66c1715d496c76909ff576619a65e Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 11:18:03 -0400 Subject: [PATCH 05/45] TEST: decrease logging level in the flush test Relates #31629 --- .../test/java/org/elasticsearch/index/shard/IndexShardIT.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 715860e6ffa..cd6a9b27b15 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -412,7 +412,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { } } - @TestLogging("_root:DEBUG,org.elasticsearch.index.shard:TRACE,org.elasticsearch.index.engine:TRACE") + @TestLogging("org.elasticsearch.index.shard:TRACE,org.elasticsearch.index.engine:TRACE") public void testStressMaybeFlushOrRollTranslogGeneration() throws Exception { createIndex("test"); ensureGreen(); From dcbbaad296f6955bdb61cf461943c3fac2984d11 Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 11:53:43 -0400 Subject: [PATCH 06/45] Mute testRecoveryWithConcurrentIndexing Relates #33473 Relates #33616 --- .../src/test/java/org/elasticsearch/upgrades/RecoveryIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java index 062016909b6..7bd5cc3a8d2 100644 --- a/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java +++ b/qa/rolling-upgrade/src/test/java/org/elasticsearch/upgrades/RecoveryIT.java @@ -111,6 +111,7 @@ public class RecoveryIT extends AbstractRollingTestCase { return future; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/33616") public void testRecoveryWithConcurrentIndexing() throws Exception { final String index = "recovery_with_concurrent_indexing"; Response response = client().performRequest(new Request("GET", "_nodes")); @@ -183,6 +184,7 @@ public class RecoveryIT extends AbstractRollingTestCase { } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/pull/33616") public void testRelocationWithConcurrentIndexing() throws Exception { final String index = "relocation_with_concurrent_indexing"; switch (CLUSTER_TYPE) { From 9600819cef27ea3dd7fe5fe93c228035e13bb320 Mon Sep 17 00:00:00 2001 From: Dimitris Athanasiou Date: Thu, 13 Sep 2018 17:13:36 +0100 Subject: [PATCH 07/45] [HLRC][ML] Add ML delete datafeed API to HLRC (#33667) Relates #29827 --- .../client/MLRequestConverters.java | 14 ++++ .../client/MachineLearningClient.java | 50 ++++++++++-- .../client/ml/DeleteDatafeedRequest.java | 80 +++++++++++++++++++ .../client/ml/DeleteJobResponse.java | 63 --------------- .../client/MLRequestConvertersTests.java | 15 ++++ .../client/MachineLearningIT.java | 20 ++++- .../MlClientDocumentationIT.java | 63 ++++++++++++++- ...s.java => DeleteDatafeedRequestTests.java} | 28 +++---- .../high-level/ml/delete-datafeed.asciidoc | 49 ++++++++++++ .../high-level/ml/delete-job.asciidoc | 2 +- .../high-level/supported-apis.asciidoc | 2 + 11 files changed, 297 insertions(+), 89 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java delete mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java rename client/rest-high-level/src/test/java/org/elasticsearch/client/ml/{DeleteJobResponseTests.java => DeleteDatafeedRequestTests.java} (51%) create mode 100644 docs/java-rest/high-level/ml/delete-datafeed.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 09c587cf81f..81b1f6b5709 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -28,6 +28,7 @@ import org.apache.http.entity.ByteArrayEntity; import org.apache.lucene.util.BytesRef; import org.elasticsearch.client.RequestConverters.EndpointBuilder; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; @@ -195,6 +196,19 @@ final class MLRequestConverters { return request; } + static Request deleteDatafeed(DeleteDatafeedRequest deleteDatafeedRequest) { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("datafeeds") + .addPathPart(deleteDatafeedRequest.getDatafeedId()) + .build(); + Request request = new Request(HttpDelete.METHOD_NAME, endpoint); + RequestConverters.Params params = new RequestConverters.Params(request); + params.putParam("force", Boolean.toString(deleteDatafeedRequest.isForce())); + return request; + } + static Request deleteForecast(DeleteForecastRequest deleteForecastRequest) throws IOException { String endpoint = new EndpointBuilder() .addPathPartAsIs("_xpack") diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 79f9267c94d..4d2167ce063 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -22,9 +22,9 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -204,11 +204,11 @@ public final class MachineLearningClient { * @return action acknowledgement * @throws IOException when there is a serialization issue sending the request or receiving the response */ - public DeleteJobResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { + public AcknowledgedResponse deleteJob(DeleteJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, MLRequestConverters::deleteJob, options, - DeleteJobResponse::fromXContent, + AcknowledgedResponse::fromXContent, Collections.emptySet()); } @@ -222,11 +222,11 @@ public final class MachineLearningClient { * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ - public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, ActionListener listener) { + public void deleteJobAsync(DeleteJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, MLRequestConverters::deleteJob, options, - DeleteJobResponse::fromXContent, + AcknowledgedResponse::fromXContent, listener, Collections.emptySet()); } @@ -492,6 +492,46 @@ public final class MachineLearningClient { Collections.emptySet()); } + /** + * Deletes the given Machine Learning Datafeed + *

+ * For additional info + * see + * ML Delete Datafeed documentation + *

+ * @param request The request to delete the datafeed + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return action acknowledgement + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public AcknowledgedResponse deleteDatafeed(DeleteDatafeedRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::deleteDatafeed, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes the given Machine Learning Datafeed asynchronously and notifies the listener on completion + *

+ * For additional info + * see + * ML Delete Datafeed documentation + *

+ * @param request The request to delete the datafeed + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void deleteDatafeedAsync(DeleteDatafeedRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::deleteDatafeed, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet()); + } + /** * Deletes Machine Learning Job Forecasts * diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java new file mode 100644 index 00000000000..1454bb590c3 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteDatafeedRequest.java @@ -0,0 +1,80 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; + +import java.util.Objects; + +/** + * Request to delete a Machine Learning Datafeed via its ID + */ +public class DeleteDatafeedRequest extends ActionRequest { + + private String datafeedId; + private boolean force; + + public DeleteDatafeedRequest(String datafeedId) { + this.datafeedId = Objects.requireNonNull(datafeedId, "[datafeed_id] must not be null"); + } + + public String getDatafeedId() { + return datafeedId; + } + + public boolean isForce() { + return force; + } + + /** + * Used to forcefully delete a started datafeed. + * This method is quicker than stopping and deleting the datafeed. + * + * @param force When {@code true} forcefully delete a started datafeed. Defaults to {@code false} + */ + public void setForce(boolean force) { + this.force = force; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public int hashCode() { + return Objects.hash(datafeedId, force); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || obj.getClass() != getClass()) { + return false; + } + + DeleteDatafeedRequest other = (DeleteDatafeedRequest) obj; + return Objects.equals(datafeedId, other.datafeedId) && Objects.equals(force, other.force); + } + +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java deleted file mode 100644 index 86cafd9e093..00000000000 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/DeleteJobResponse.java +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch 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 org.elasticsearch.client.ml; - -import org.elasticsearch.action.support.master.AcknowledgedResponse; -import org.elasticsearch.common.xcontent.XContentParser; - -import java.io.IOException; -import java.util.Objects; - -/** - * Response acknowledging the Machine Learning Job request - */ -public class DeleteJobResponse extends AcknowledgedResponse { - - public DeleteJobResponse(boolean acknowledged) { - super(acknowledged); - } - - public DeleteJobResponse() { - } - - public static DeleteJobResponse fromXContent(XContentParser parser) throws IOException { - AcknowledgedResponse response = AcknowledgedResponse.fromXContent(parser); - return new DeleteJobResponse(response.isAcknowledged()); - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - - if (other == null || getClass() != other.getClass()) { - return false; - } - - DeleteJobResponse that = (DeleteJobResponse) other; - return isAcknowledged() == that.isAcknowledged(); - } - - @Override - public int hashCode() { - return Objects.hash(isAcknowledged()); - } - -} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 19db672e35b..547bc2e9a93 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -24,6 +24,7 @@ import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpPut; import org.elasticsearch.client.ml.CloseJobRequest; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; import org.elasticsearch.client.ml.FlushJobRequest; @@ -223,6 +224,20 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testDeleteDatafeed() { + String datafeedId = randomAlphaOfLength(10); + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + + Request request = MLRequestConverters.deleteDatafeed(deleteDatafeedRequest); + assertEquals(HttpDelete.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/datafeeds/" + datafeedId, request.getEndpoint()); + assertEquals(Boolean.toString(false), request.getParameters().get("force")); + + deleteDatafeedRequest.setForce(true); + request = MLRequestConverters.deleteDatafeed(deleteDatafeedRequest); + assertEquals(Boolean.toString(true), request.getParameters().get("force")); + } + public void testDeleteForecast() throws Exception { String jobId = randomAlphaOfLength(10); DeleteForecastRequest deleteForecastRequest = new DeleteForecastRequest(jobId); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index c0bf1055058..a07b4414843 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -25,9 +25,9 @@ import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.support.master.AcknowledgedResponse; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -129,7 +129,7 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); - DeleteJobResponse response = execute(new DeleteJobRequest(jobId), + AcknowledgedResponse response = execute(new DeleteJobRequest(jobId), machineLearningClient::deleteJob, machineLearningClient::deleteJobAsync); @@ -312,6 +312,22 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { assertThat(createdDatafeed.getIndices(), equalTo(datafeedConfig.getIndices())); } + public void testDeleteDatafeed() throws Exception { + String jobId = randomValidJobId(); + Job job = buildJob(jobId); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + machineLearningClient.putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String datafeedId = "datafeed-" + jobId; + DatafeedConfig datafeedConfig = DatafeedConfig.builder(datafeedId, jobId).setIndices("some_data_index").build(); + execute(new PutDatafeedRequest(datafeedConfig), machineLearningClient::putDatafeed, machineLearningClient::putDatafeedAsync); + + AcknowledgedResponse response = execute(new DeleteDatafeedRequest(datafeedId), machineLearningClient::deleteDatafeed, + machineLearningClient::deleteDatafeedAsync); + + assertTrue(response.isAcknowledged()); + } + public void testDeleteForecast() throws Exception { String jobId = "test-delete-forecast"; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 3e43792ac6a..09d32710eb1 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -34,9 +34,9 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.RestHighLevelClient; import org.elasticsearch.client.ml.CloseJobRequest; import org.elasticsearch.client.ml.CloseJobResponse; +import org.elasticsearch.client.ml.DeleteDatafeedRequest; import org.elasticsearch.client.ml.DeleteForecastRequest; import org.elasticsearch.client.ml.DeleteJobRequest; -import org.elasticsearch.client.ml.DeleteJobResponse; import org.elasticsearch.client.ml.FlushJobRequest; import org.elasticsearch.client.ml.FlushJobResponse; import org.elasticsearch.client.ml.ForecastJobRequest; @@ -264,7 +264,7 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { //tag::x-pack-delete-ml-job-request DeleteJobRequest deleteJobRequest = new DeleteJobRequest("my-first-machine-learning-job"); deleteJobRequest.setForce(false); //<1> - DeleteJobResponse deleteJobResponse = client.machineLearning().deleteJob(deleteJobRequest, RequestOptions.DEFAULT); + AcknowledgedResponse deleteJobResponse = client.machineLearning().deleteJob(deleteJobRequest, RequestOptions.DEFAULT); //end::x-pack-delete-ml-job-request //tag::x-pack-delete-ml-job-response @@ -273,9 +273,9 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { } { //tag::x-pack-delete-ml-job-request-listener - ActionListener listener = new ActionListener() { + ActionListener listener = new ActionListener() { @Override - public void onResponse(DeleteJobResponse deleteJobResponse) { + public void onResponse(AcknowledgedResponse acknowledgedResponse) { // <1> } @@ -587,6 +587,61 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { } } + public void testDeleteDatafeed() throws Exception { + RestHighLevelClient client = highLevelClient(); + + String jobId = "test-delete-datafeed-job"; + Job job = MachineLearningIT.buildJob(jobId); + client.machineLearning().putJob(new PutJobRequest(job), RequestOptions.DEFAULT); + + String datafeedId = "test-delete-datafeed"; + DatafeedConfig datafeed = DatafeedConfig.builder(datafeedId, jobId).setIndices("foo").build(); + client.machineLearning().putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT); + + { + //tag::x-pack-delete-ml-datafeed-request + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + deleteDatafeedRequest.setForce(false); //<1> + AcknowledgedResponse deleteDatafeedResponse = client.machineLearning().deleteDatafeed( + deleteDatafeedRequest, RequestOptions.DEFAULT); + //end::x-pack-delete-ml-datafeed-request + + //tag::x-pack-delete-ml-datafeed-response + boolean isAcknowledged = deleteDatafeedResponse.isAcknowledged(); //<1> + //end::x-pack-delete-ml-datafeed-response + } + + // Recreate datafeed to allow second deletion + client.machineLearning().putDatafeed(new PutDatafeedRequest(datafeed), RequestOptions.DEFAULT); + + { + //tag::x-pack-delete-ml-datafeed-request-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(AcknowledgedResponse acknowledgedResponse) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + //end::x-pack-delete-ml-datafeed-request-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + //tag::x-pack-delete-ml-datafeed-request-async + DeleteDatafeedRequest deleteDatafeedRequest = new DeleteDatafeedRequest(datafeedId); + client.machineLearning().deleteDatafeedAsync(deleteDatafeedRequest, RequestOptions.DEFAULT, listener); // <1> + //end::x-pack-delete-ml-datafeed-request-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + public void testGetBuckets() throws IOException, InterruptedException { RestHighLevelClient client = highLevelClient(); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java similarity index 51% rename from client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java rename to client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java index 2eb4d51e191..e36aa855a7b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteJobResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/DeleteDatafeedRequestTests.java @@ -18,25 +18,25 @@ */ package org.elasticsearch.client.ml; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; +import org.elasticsearch.test.ESTestCase; -import java.io.IOException; +public class DeleteDatafeedRequestTests extends ESTestCase { -public class DeleteJobResponseTests extends AbstractXContentTestCase { - - @Override - protected DeleteJobResponse createTestInstance() { - return new DeleteJobResponse(); + public void testConstructor_GivenNullId() { + NullPointerException ex = expectThrows(NullPointerException.class, () -> new DeleteJobRequest(null)); + assertEquals("[job_id] must not be null", ex.getMessage()); } - @Override - protected DeleteJobResponse doParseInstance(XContentParser parser) throws IOException { - return DeleteJobResponse.fromXContent(parser); + public void testSetForce() { + DeleteDatafeedRequest deleteDatafeedRequest = createTestInstance(); + assertFalse(deleteDatafeedRequest.isForce()); + + deleteDatafeedRequest.setForce(true); + assertTrue(deleteDatafeedRequest.isForce()); } - @Override - protected boolean supportsUnknownFields() { - return false; + private DeleteDatafeedRequest createTestInstance() { + return new DeleteDatafeedRequest(DatafeedConfigTests.randomValidDatafeedId()); } } diff --git a/docs/java-rest/high-level/ml/delete-datafeed.asciidoc b/docs/java-rest/high-level/ml/delete-datafeed.asciidoc new file mode 100644 index 00000000000..68741651b33 --- /dev/null +++ b/docs/java-rest/high-level/ml/delete-datafeed.asciidoc @@ -0,0 +1,49 @@ +[[java-rest-high-x-pack-ml-delete-datafeed]] +=== Delete Datafeed API + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-request]] +==== Delete Datafeed Request + +A `DeleteDatafeedRequest` object requires a non-null `datafeedId` and can optionally set `force`. +Can be executed as follows: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request] +--------------------------------------------------- +<1> Use to forcefully delete a started datafeed; +this method is quicker than stopping and deleting the datafeed. +Defaults to `false`. + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-response]] +==== Delete Datafeed Response + +The returned `AcknowledgedResponse` object indicates the acknowledgement of the request: +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-response] +--------------------------------------------------- +<1> `isAcknowledged` was the deletion request acknowledged or not + +[[java-rest-high-x-pack-machine-learning-delete-datafeed-async]] +==== Delete Datafeed Asynchronously + +This request can also be made asynchronously. +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request-async] +--------------------------------------------------- +<1> The `DeleteDatafeedRequest` to execute and the `ActionListener` to alert on completion or error. + +The deletion request returns immediately. Once the request is completed, the `ActionListener` is +called back using the `onResponse` or `onFailure`. The latter indicates some failure occurred when +making the request. + +A typical listener for a `DeleteDatafeedRequest` could be defined as follows: + +["source","java",subs="attributes,callouts,macros"] +--------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-datafeed-request-listener] +--------------------------------------------------- +<1> The action to be taken when it is completed +<2> What to do when a failure occurs diff --git a/docs/java-rest/high-level/ml/delete-job.asciidoc b/docs/java-rest/high-level/ml/delete-job.asciidoc index 44a6a479409..43f1e2fb02b 100644 --- a/docs/java-rest/high-level/ml/delete-job.asciidoc +++ b/docs/java-rest/high-level/ml/delete-job.asciidoc @@ -18,7 +18,7 @@ Defaults to `false` [[java-rest-high-x-pack-machine-learning-delete-job-response]] ==== Delete Job Response -The returned `DeleteJobResponse` object indicates the acknowledgement of the request: +The returned `AcknowledgedResponse` object indicates the acknowledgement of the request: ["source","java",subs="attributes,callouts,macros"] --------------------------------------------------- include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-delete-ml-job-response] diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index 0be681a14d1..cb297d0f712 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -221,6 +221,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> * <> * <> * <> @@ -238,6 +239,7 @@ include::ml/close-job.asciidoc[] include::ml/update-job.asciidoc[] include::ml/flush-job.asciidoc[] include::ml/put-datafeed.asciidoc[] +include::ml/delete-datafeed.asciidoc[] include::ml/get-job-stats.asciidoc[] include::ml/forecast-job.asciidoc[] include::ml/delete-forecast.asciidoc[] From c3a817957d6ff02d0412c37d28c916457e2d9531 Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Thu, 13 Sep 2018 10:42:26 -0700 Subject: [PATCH 08/45] [DOCS] Moves securing-communications to docs (#33640) --- .../configuring-tls-docker.asciidoc | 0 .../enabling-cipher-suites.asciidoc | 0 .../node-certificates.asciidoc | 0 .../securing-elasticsearch.asciidoc | 10 +++++----- .../separating-node-client-traffic.asciidoc | 4 ++-- .../setting-up-ssl.asciidoc | 0 .../securing-communications/tls-ad.asciidoc | 0 .../securing-communications/tls-http.asciidoc | 0 .../securing-communications/tls-ldap.asciidoc | 0 .../tls-transport.asciidoc | 0 x-pack/docs/en/security/configuring-es.asciidoc | 16 ++++++++++++---- .../en/security/securing-communications.asciidoc | 6 ++---- 12 files changed, 21 insertions(+), 15 deletions(-) rename {x-pack/docs/en => docs/reference}/security/securing-communications/configuring-tls-docker.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/enabling-cipher-suites.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/node-certificates.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/securing-elasticsearch.asciidoc (84%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/separating-node-client-traffic.asciidoc (94%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/setting-up-ssl.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-ad.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-http.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-ldap.asciidoc (100%) rename {x-pack/docs/en => docs/reference}/security/securing-communications/tls-transport.asciidoc (100%) diff --git a/x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc b/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/configuring-tls-docker.asciidoc rename to docs/reference/security/securing-communications/configuring-tls-docker.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/enabling-cipher-suites.asciidoc b/docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/enabling-cipher-suites.asciidoc rename to docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/node-certificates.asciidoc b/docs/reference/security/securing-communications/node-certificates.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/node-certificates.asciidoc rename to docs/reference/security/securing-communications/node-certificates.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc b/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc similarity index 84% rename from x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc rename to docs/reference/security/securing-communications/securing-elasticsearch.asciidoc index 09cb118f684..6b919e065c6 100644 --- a/x-pack/docs/en/security/securing-communications/securing-elasticsearch.asciidoc +++ b/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc @@ -29,17 +29,17 @@ information, see <>. For more information about encrypting communications across the Elastic Stack, see {xpack-ref}/encrypting-communications.html[Encrypting Communications]. -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/node-certificates.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/node-certificates.asciidoc include::node-certificates.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-transport.asciidoc include::tls-transport.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-http.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-http.asciidoc include::tls-http.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ad.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-ad.asciidoc include::tls-ad.asciidoc[] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/tls-ldap.asciidoc include::tls-ldap.asciidoc[] \ No newline at end of file diff --git a/x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc b/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc similarity index 94% rename from x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc rename to docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc index 887d4701d78..e911ad529c4 100644 --- a/x-pack/docs/en/security/securing-communications/separating-node-client-traffic.asciidoc +++ b/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc @@ -37,7 +37,7 @@ transport.profiles.client.bind_host: 1.1.1.1 <2> <2> The bind address for the network used for client communication If separate networks are not available, then -{xpack-ref}/ip-filtering.html[IP Filtering] can +{stack-ov}/ip-filtering.html[IP Filtering] can be enabled to limit access to the profiles. When using SSL for transport, a different set of certificates can also be used @@ -65,4 +65,4 @@ transport.profiles.client.xpack.security.ssl.client_authentication: none This setting keeps certificate authentication active for node-to-node traffic, but removes the requirement to distribute a signed certificate to transport clients. For more information, see -{xpack-ref}/java-clients.html#transport-client[Configuring the Transport Client to work with a Secured Cluster]. +{stack-ov}/java-clients.html#transport-client[Configuring the Transport Client to work with a Secured Cluster]. diff --git a/x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc b/docs/reference/security/securing-communications/setting-up-ssl.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc rename to docs/reference/security/securing-communications/setting-up-ssl.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-ad.asciidoc b/docs/reference/security/securing-communications/tls-ad.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-ad.asciidoc rename to docs/reference/security/securing-communications/tls-ad.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-http.asciidoc b/docs/reference/security/securing-communications/tls-http.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-http.asciidoc rename to docs/reference/security/securing-communications/tls-http.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc b/docs/reference/security/securing-communications/tls-ldap.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-ldap.asciidoc rename to docs/reference/security/securing-communications/tls-ldap.asciidoc diff --git a/x-pack/docs/en/security/securing-communications/tls-transport.asciidoc b/docs/reference/security/securing-communications/tls-transport.asciidoc similarity index 100% rename from x-pack/docs/en/security/securing-communications/tls-transport.asciidoc rename to docs/reference/security/securing-communications/tls-transport.asciidoc diff --git a/x-pack/docs/en/security/configuring-es.asciidoc b/x-pack/docs/en/security/configuring-es.asciidoc index 5fd9ed610cb..7bdfbef08de 100644 --- a/x-pack/docs/en/security/configuring-es.asciidoc +++ b/x-pack/docs/en/security/configuring-es.asciidoc @@ -136,10 +136,15 @@ By default, events are logged to a dedicated `elasticsearch-access.log` file in easier analysis and control what events are logged. -- -include::securing-communications/securing-elasticsearch.asciidoc[] -include::securing-communications/configuring-tls-docker.asciidoc[] -include::securing-communications/enabling-cipher-suites.asciidoc[] -include::securing-communications/separating-node-client-traffic.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/securing-elasticsearch.asciidoc +include::{es-repo-dir}/security/securing-communications/securing-elasticsearch.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/configuring-tls-docker.asciidoc +include::{es-repo-dir}/security/securing-communications/configuring-tls-docker.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/enabling-cipher-suites.asciidoc +include::{es-repo-dir}/security/securing-communications/enabling-cipher-suites.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/separating-node-client-traffic.asciidoc +include::{es-repo-dir}/security/securing-communications/separating-node-client-traffic.asciidoc[] +:edit_url: include::authentication/configuring-active-directory-realm.asciidoc[] include::authentication/configuring-file-realm.asciidoc[] include::authentication/configuring-ldap-realm.asciidoc[] @@ -148,6 +153,9 @@ include::authentication/configuring-pki-realm.asciidoc[] include::authentication/configuring-saml-realm.asciidoc[] :edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc include::authentication/configuring-kerberos-realm.asciidoc[] +:edit_url: include::fips-140-compliance.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/security-settings.asciidoc include::{es-repo-dir}/settings/security-settings.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/settings/audit-settings.asciidoc include::{es-repo-dir}/settings/audit-settings.asciidoc[] diff --git a/x-pack/docs/en/security/securing-communications.asciidoc b/x-pack/docs/en/security/securing-communications.asciidoc index 11f6b3dc561..84f3b0bc27a 100644 --- a/x-pack/docs/en/security/securing-communications.asciidoc +++ b/x-pack/docs/en/security/securing-communications.asciidoc @@ -17,10 +17,8 @@ This section shows how to: The authentication of new nodes helps prevent a rogue node from joining the cluster and receiving data through replication. -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/securing-communications/setting-up-ssl.asciidoc -include::securing-communications/setting-up-ssl.asciidoc[] - -//TO-DO: These sections can be removed when all links to them are removed. +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/securing-communications/setting-up-ssl.asciidoc +include::{es-repo-dir}/security/securing-communications/setting-up-ssl.asciidoc[] [[ciphers]] === Enabling cipher suites for stronger encryption From 7e51b960fbc109b916dfb321eb05b44358a2bd17 Mon Sep 17 00:00:00 2001 From: Benjamin Trent Date: Thu, 13 Sep 2018 10:44:33 -0700 Subject: [PATCH 09/45] Adding index refresh (#33647) --- .../xpack/ml/action/TransportDeleteForecastAction.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java index d80bbd1b342..e91f75964fc 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/action/TransportDeleteForecastAction.java @@ -211,6 +211,7 @@ public class TransportDeleteForecastAction extends HandledTransportAction Date: Thu, 13 Sep 2018 13:55:08 -0400 Subject: [PATCH 10/45] Revert "Use serializable exception in GCP listeners (#33657)" This reverts commit 6dfe54c8381e2b9046de836fb07fabaa03a02452. --- .../index/shard/GlobalCheckpointListeners.java | 13 ++++++------- .../index/shard/GlobalCheckpointListenersTests.java | 9 ++++----- .../org/elasticsearch/index/shard/IndexShardIT.java | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index e738ebac160..224d5be17e1 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,7 +21,6 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -34,6 +33,7 @@ import java.util.concurrent.Executor; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -53,8 +53,7 @@ public class GlobalCheckpointListeners implements Closeable { * Callback when the global checkpoint is updated or the shard is closed. If the shard is closed, the value of the global checkpoint * will be set to {@link org.elasticsearch.index.seqno.SequenceNumbers#UNASSIGNED_SEQ_NO} and the exception will be non-null and an * instance of {@link IndexShardClosedException }. If the listener timed out waiting for notification then the exception will be - * non-null and an instance of {@link ElasticsearchTimeoutException}. If the global checkpoint is updated, the exception will be - * null. + * non-null and an instance of {@link TimeoutException}. If the global checkpoint is updated, the exception will be null. * * @param globalCheckpoint the updated global checkpoint * @param e if non-null, the shard is closed or the listener timed out @@ -97,8 +96,8 @@ public class GlobalCheckpointListeners implements Closeable { * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be - * notified after if the timeout elapses. In this case, the listener will be notified with a {@link ElasticsearchTimeoutException}. - * Passing null for the timeout means no timeout will be associated to the listener. + * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for + * the timeout means no timeout will be associated to the listener. * * @param currentGlobalCheckpoint the current global checkpoint known to the listener * @param listener the listener @@ -141,7 +140,7 @@ public class GlobalCheckpointListeners implements Closeable { removed = listeners != null && listeners.remove(listener) != null; } if (removed) { - final ElasticsearchTimeoutException e = new ElasticsearchTimeoutException(timeout.getStringRep()); + final TimeoutException e = new TimeoutException(timeout.getStringRep()); logger.trace("global checkpoint listener timed out", e); executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); } @@ -226,7 +225,7 @@ public class GlobalCheckpointListeners implements Closeable { } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); } else { - assert e instanceof ElasticsearchTimeoutException : e; + assert e instanceof TimeoutException : e; logger.warn("error notifying global checkpoint listener of timeout", caught); } } diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 4a6383c50d2..4ab278cc02a 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,7 +21,6 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -43,6 +42,7 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -515,11 +515,10 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); assertThat(e, hasToString(containsString(timeout.getStringRep()))); final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); - final ArgumentCaptor t = - ArgumentCaptor.forClass(ElasticsearchTimeoutException.class); + final ArgumentCaptor t = ArgumentCaptor.forClass(TimeoutException.class); verify(mockLogger).trace(message.capture(), t.capture()); assertThat(message.getValue(), equalTo("global checkpoint listener timed out")); assertThat(t.getValue(), hasToString(containsString(timeout.getStringRep()))); @@ -551,7 +550,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); } finally { latch.countDown(); } diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index cd6a9b27b15..2c659ac60ec 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.index.shard; import org.apache.lucene.store.LockObtainFailedException; -import org.elasticsearch.ElasticsearchTimeoutException; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -90,6 +89,7 @@ import java.util.Locale; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -798,7 +798,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); assertNotNull(e); - assertThat(e, instanceOf(ElasticsearchTimeoutException.class)); + assertThat(e, instanceOf(TimeoutException.class)); assertThat(e.getMessage(), equalTo(timeout.getStringRep())); } finally { latch.countDown(); From 040695b64e6452c9e54109b9d31fe3d1efdd69f7 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Thu, 13 Sep 2018 20:45:48 +0200 Subject: [PATCH 11/45] CORE: Disable Setting Type Validation (#33660) (#33669) * Reverts setting type validation introduced in #33503 --- .../main/java/org/elasticsearch/common/settings/Setting.java | 2 +- .../java/org/elasticsearch/common/settings/SettingTests.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/org/elasticsearch/common/settings/Setting.java b/server/src/main/java/org/elasticsearch/common/settings/Setting.java index 5244cdd726d..23984e58749 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/Setting.java +++ b/server/src/main/java/org/elasticsearch/common/settings/Setting.java @@ -458,7 +458,7 @@ public class Setting implements ToXContentObject { * @return the raw string representation of the setting value */ String innerGetRaw(final Settings settings) { - return settings.get(getKey(), defaultValue.apply(settings), isListSetting()); + return settings.get(getKey(), defaultValue.apply(settings)); } /** Logs a deprecation warning if the setting is deprecated and used. */ diff --git a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java index 30cfee81ddd..99fde0855f9 100644 --- a/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java +++ b/server/src/test/java/org/elasticsearch/common/settings/SettingTests.java @@ -180,6 +180,7 @@ public class SettingTests extends ESTestCase { } } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33135") public void testValidateStringSetting() { Settings settings = Settings.builder().putList("foo.bar", Arrays.asList("bla-a", "bla-b")).build(); Setting stringSetting = Setting.simpleString("foo.bar", Property.NodeScope); From b9d0c8f25cf6535e2f4583bc4cceaf44e5913501 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 20:47:24 +0200 Subject: [PATCH 12/45] [CCR] Add monitoring mapping verification test (#33662) Added test that verifies that all fields in ShardFollowNodeTaskStatus are mapped in monitoring-es.json --- .../ccr/CcrStatsMonitoringDocTests.java | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java index 70b73e5eed0..ed893410c88 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java @@ -7,12 +7,16 @@ package org.elasticsearch.xpack.monitoring.collector.ccr; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.common.xcontent.support.XContentMapValues; import org.elasticsearch.xpack.core.ccr.ShardFollowNodeTaskStatus; import org.elasticsearch.xpack.core.monitoring.MonitoredSystem; import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringDoc; +import org.elasticsearch.xpack.core.monitoring.exporter.MonitoringTemplateUtils; import org.elasticsearch.xpack.monitoring.exporter.BaseMonitoringDocTestCase; import org.joda.time.DateTime; import org.joda.time.DateTimeZone; @@ -20,13 +24,17 @@ import org.junit.Before; import java.io.IOException; import java.util.Collections; +import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; +import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; +import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasToString; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; @@ -174,4 +182,65 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase fetchExceptions = + new TreeMap<>(Collections.singletonMap(1L, new ElasticsearchException("shard is sad"))); + final ShardFollowNodeTaskStatus status = new ShardFollowNodeTaskStatus( + "cluster_alias:leader_index", + "follower_index", + 0, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 100, + 10, + 0, + 10, + 100, + 10, + 10, + 0, + 10, + fetchExceptions, + 2); + XContentBuilder builder = jsonBuilder(); + builder.value(status); + Map serializedStatus = XContentHelper.convertToMap(XContentType.JSON.xContent(), Strings.toString(builder), false); + + Map template = + XContentHelper.convertToMap(XContentType.JSON.xContent(), MonitoringTemplateUtils.loadTemplate("es"), false); + Map ccrStatsMapping = (Map) XContentMapValues.extractValue("mappings.doc.properties.ccr_stats.properties", template); + + assertThat(serializedStatus.size(), equalTo(ccrStatsMapping.size())); + for (Map.Entry entry : serializedStatus.entrySet()) { + String fieldName = entry.getKey(); + Map fieldMapping = (Map) ccrStatsMapping.get(fieldName); + assertThat(fieldMapping, notNullValue()); + + Object fieldValue = entry.getValue(); + String fieldType = (String) fieldMapping.get("type"); + if (fieldValue instanceof Long || fieldValue instanceof Integer) { + assertThat("expected long field type for field [" + fieldName + "]", fieldType, + anyOf(equalTo("long"), equalTo("integer"))); + } else if (fieldValue instanceof String) { + assertThat("expected keyword field type for field [" + fieldName + "]", fieldType, + anyOf(equalTo("keyword"), equalTo("text"))); + } else { + // Manual test specific object fields and if not just fail: + if (fieldName.equals("fetch_exceptions")) { + assertThat(XContentMapValues.extractValue("properties.from_seq_no.type", fieldMapping), equalTo("long")); + assertThat(XContentMapValues.extractValue("properties.exception.type", fieldMapping), equalTo("text")); + } else { + fail("unexpected field value type [" + fieldValue.getClass() + "] for field [" + fieldName + "]"); + } + } + } + } + } From 53ba253aa4602158881814609872435e6d66feeb Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Thu, 13 Sep 2018 20:52:00 +0200 Subject: [PATCH 13/45] [CCR] Add validation for max_retry_delay (#33648) --- .../ccr/action/FollowIndexRequestTests.java | 22 ++++++++++ .../xpack/core/ccr/AutoFollowMetadata.java | 2 +- .../core/ccr/action/FollowIndexAction.java | 32 +++++++++++---- .../action/PutAutoFollowPatternAction.java | 19 ++++++++- .../PutAutoFollowPatternRequestTests.java | 41 +++++++++++++++++++ 5 files changed, 105 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java index 2017fa2fdb9..e5f7e693a7f 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/FollowIndexRequestTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ccr.action; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; @@ -12,6 +13,10 @@ import org.elasticsearch.xpack.core.ccr.action.FollowIndexAction; import java.io.IOException; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; + public class FollowIndexRequestTests extends AbstractStreamableXContentTestCase { @Override @@ -39,4 +44,21 @@ public class FollowIndexRequestTests extends AbstractStreamableXContentTestCase< randomIntBetween(1, Integer.MAX_VALUE), randomNonNegativeLong(), randomIntBetween(1, Integer.MAX_VALUE), randomIntBetween(1, Integer.MAX_VALUE), TimeValue.timeValueMillis(500), TimeValue.timeValueMillis(500)); } + + public void testValidate() { + FollowIndexAction.Request request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, + null, TimeValue.ZERO, null); + ActionRequestValidationException validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be positive but was [0ms]")); + + request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, null, TimeValue.timeValueMinutes(10), null); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be less than [5m] but was [10m]")); + + request = new FollowIndexAction.Request("index1", "index2", null, null, null, null, null, TimeValue.timeValueMinutes(1), null); + validationException = request.validate(); + assertThat(validationException, nullValue()); + } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java index 71fd13d0b50..9c64ea3da76 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadata.java @@ -169,7 +169,7 @@ public class AutoFollowMetadata extends AbstractNamedDiffable i public static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); public static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); public static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - public static final ParseField MAX_RETRY_DELAY = new ParseField("retry_timeout"); + public static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); public static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); @SuppressWarnings("unchecked") diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java index 2c311356d49..d5a0b0408c5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/FollowIndexAction.java @@ -23,6 +23,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.action.ValidateActions.addValidationError; + public final class FollowIndexAction extends Action { public static final FollowIndexAction INSTANCE = new FollowIndexAction(); @@ -33,8 +35,9 @@ public final class FollowIndexAction extends Action { public static final int DEFAULT_MAX_CONCURRENT_READ_BATCHES = 1; public static final int DEFAULT_MAX_CONCURRENT_WRITE_BATCHES = 1; public static final long DEFAULT_MAX_BATCH_SIZE_IN_BYTES = Long.MAX_VALUE; - public static final TimeValue DEFAULT_RETRY_TIMEOUT = new TimeValue(500); - public static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); + static final TimeValue DEFAULT_MAX_RETRY_DELAY = new TimeValue(500); + static final TimeValue DEFAULT_IDLE_SHARD_RETRY_DELAY = TimeValue.timeValueSeconds(10); + static final TimeValue MAX_RETRY_DELAY = TimeValue.timeValueMinutes(5); private FollowIndexAction() { super(NAME); @@ -54,7 +57,7 @@ public final class FollowIndexAction extends Action { private static final ParseField MAX_BATCH_SIZE_IN_BYTES = new ParseField("max_batch_size_in_bytes"); private static final ParseField MAX_CONCURRENT_WRITE_BATCHES = new ParseField("max_concurrent_write_batches"); private static final ParseField MAX_WRITE_BUFFER_SIZE = new ParseField("max_write_buffer_size"); - private static final ParseField MAX_RETRY_DELAY = new ParseField("max_retry_delay"); + private static final ParseField MAX_RETRY_DELAY_FIELD = new ParseField("max_retry_delay"); private static final ParseField IDLE_SHARD_RETRY_DELAY = new ParseField("idle_shard_retry_delay"); private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>(NAME, true, (args, followerIndex) -> { @@ -75,8 +78,8 @@ public final class FollowIndexAction extends Action { PARSER.declareInt(ConstructingObjectParser.optionalConstructorArg(), MAX_WRITE_BUFFER_SIZE); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), - (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY.getPreferredName()), - MAX_RETRY_DELAY, + (p, c) -> TimeValue.parseTimeValue(p.text(), MAX_RETRY_DELAY_FIELD.getPreferredName()), + MAX_RETRY_DELAY_FIELD, ObjectParser.ValueType.STRING); PARSER.declareField( ConstructingObjectParser.optionalConstructorArg(), @@ -202,7 +205,7 @@ public final class FollowIndexAction extends Action { throw new IllegalArgumentException(MAX_WRITE_BUFFER_SIZE.getPreferredName() + " must be larger than 0"); } - final TimeValue actualRetryTimeout = maxRetryDelay == null ? DEFAULT_RETRY_TIMEOUT : maxRetryDelay; + final TimeValue actualRetryTimeout = maxRetryDelay == null ? DEFAULT_MAX_RETRY_DELAY : maxRetryDelay; final TimeValue actualIdleShardRetryDelay = idleShardRetryDelay == null ? DEFAULT_IDLE_SHARD_RETRY_DELAY : idleShardRetryDelay; this.leaderIndex = leaderIndex; @@ -222,7 +225,20 @@ public final class FollowIndexAction extends Action { @Override public ActionRequestValidationException validate() { - return null; + ActionRequestValidationException validationException = null; + + if (maxRetryDelay.millis() <= 0) { + String message = "[" + MAX_RETRY_DELAY_FIELD.getPreferredName() + "] must be positive but was [" + + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + if (maxRetryDelay.millis() > FollowIndexAction.MAX_RETRY_DELAY.millis()) { + String message = "[" + MAX_RETRY_DELAY_FIELD.getPreferredName() + "] must be less than [" + MAX_RETRY_DELAY + + "] but was [" + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + + return validationException; } @Override @@ -264,7 +280,7 @@ public final class FollowIndexAction extends Action { builder.field(MAX_WRITE_BUFFER_SIZE.getPreferredName(), maxWriteBufferSize); builder.field(MAX_CONCURRENT_READ_BATCHES.getPreferredName(), maxConcurrentReadBatches); builder.field(MAX_CONCURRENT_WRITE_BATCHES.getPreferredName(), maxConcurrentWriteBatches); - builder.field(MAX_RETRY_DELAY.getPreferredName(), maxRetryDelay.getStringRep()); + builder.field(MAX_RETRY_DELAY_FIELD.getPreferredName(), maxRetryDelay.getStringRep()); builder.field(IDLE_SHARD_RETRY_DELAY.getPreferredName(), idleShardRetryDelay.getStringRep()); } builder.endObject(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java index dc69795bb4a..01ebd3f1d81 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternAction.java @@ -94,10 +94,25 @@ public class PutAutoFollowPatternAction extends Action { public ActionRequestValidationException validate() { ActionRequestValidationException validationException = null; if (leaderClusterAlias == null) { - validationException = addValidationError("leaderClusterAlias is missing", validationException); + validationException = addValidationError("[" + LEADER_CLUSTER_ALIAS_FIELD.getPreferredName() + + "] is missing", validationException); } if (leaderIndexPatterns == null || leaderIndexPatterns.isEmpty()) { - validationException = addValidationError("leaderIndexPatterns is missing", validationException); + validationException = addValidationError("[" + LEADER_INDEX_PATTERNS_FIELD.getPreferredName() + + "] is missing", validationException); + } + if (maxRetryDelay != null) { + if (maxRetryDelay.millis() <= 0) { + String message = "[" + AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName() + "] must be positive but was [" + + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } + if (maxRetryDelay.millis() > FollowIndexAction.MAX_RETRY_DELAY.millis()) { + String message = "[" + AutoFollowPattern.MAX_RETRY_DELAY.getPreferredName() + "] must be less than [" + + FollowIndexAction.MAX_RETRY_DELAY + + "] but was [" + maxRetryDelay.getStringRep() + "]"; + validationException = addValidationError(message, validationException); + } } return validationException; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java index f11e1885e80..ced49bbae12 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java @@ -5,12 +5,18 @@ */ package org.elasticsearch.xpack.core.ccr.action; +import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; import java.io.IOException; import java.util.Arrays; +import java.util.Collections; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; public class PutAutoFollowPatternRequestTests extends AbstractStreamableXContentTestCase { @@ -60,4 +66,39 @@ public class PutAutoFollowPatternRequestTests extends AbstractStreamableXContent } return request; } + + public void testValidate() { + PutAutoFollowPatternAction.Request request = new PutAutoFollowPatternAction.Request(); + ActionRequestValidationException validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_cluster_alias] is missing")); + + request.setLeaderClusterAlias("_alias"); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_index_patterns] is missing")); + + request.setLeaderIndexPatterns(Collections.emptyList()); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[leader_index_patterns] is missing")); + + request.setLeaderIndexPatterns(Collections.singletonList("logs-*")); + validationException = request.validate(); + assertThat(validationException, nullValue()); + + request.setMaxRetryDelay(TimeValue.ZERO); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be positive but was [0ms]")); + + request.setMaxRetryDelay(TimeValue.timeValueMinutes(10)); + validationException = request.validate(); + assertThat(validationException, notNullValue()); + assertThat(validationException.getMessage(), containsString("[max_retry_delay] must be less than [5m] but was [10m]")); + + request.setMaxRetryDelay(TimeValue.timeValueMinutes(1)); + validationException = request.validate(); + assertThat(validationException, nullValue()); + } } From 60ab4f97ab15c138412e2af10deba57a6b4e5e9d Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Thu, 13 Sep 2018 21:55:44 +0300 Subject: [PATCH 14/45] SQL: Return correct catalog separator in JDBC (#33670) JDBC DatabaseMetadata returns correct separator (:) for catalog/cluster names. Fix #33654 --- .../sql/jdbc/jdbc/JdbcDatabaseMetaData.java | 2 +- .../jdbc/jdbc/JdbcDatabaseMetaDataTests.java | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java index 5cb63a33763..d2e24f3edac 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java @@ -368,7 +368,7 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getCatalogSeparator() throws SQLException { - return "."; + return ":"; } @Override diff --git a/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java new file mode 100644 index 00000000000..cfa6e797260 --- /dev/null +++ b/x-pack/plugin/sql/jdbc/src/test/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaDataTests.java @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +package org.elasticsearch.xpack.sql.jdbc.jdbc; + +import org.elasticsearch.test.ESTestCase; + +public class JdbcDatabaseMetaDataTests extends ESTestCase { + + private JdbcDatabaseMetaData md = new JdbcDatabaseMetaData(null); + + public void testSeparators() throws Exception { + assertEquals(":", md.getCatalogSeparator()); + assertEquals("\"", md.getIdentifierQuoteString()); + assertEquals("\\", md.getSearchStringEscape()); + + } +} From 32a22ca00e5329b785e4aa248677aab7d3349191 Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Thu, 13 Sep 2018 22:07:23 +0300 Subject: [PATCH 15/45] DOC: improved wording in SQL client app section --- docs/reference/sql/endpoints/client-apps/index.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reference/sql/endpoints/client-apps/index.asciidoc b/docs/reference/sql/endpoints/client-apps/index.asciidoc index ee9891040d0..2c497518328 100644 --- a/docs/reference/sql/endpoints/client-apps/index.asciidoc +++ b/docs/reference/sql/endpoints/client-apps/index.asciidoc @@ -3,7 +3,7 @@ [[sql-client-apps]] == SQL Client Applications -Thanks to its <> interface, {es-sql} supports a broad range of applications. +Thanks to its <> interface, a broad range of third-party applications can use {es}'s SQL capabilities. This section lists, in alphabetical order, a number of them and their respective configuration - the list however is by no means comprehensive (feel free to https://www.elastic.co/blog/art-of-pull-request[submit a PR] to improve it): as long as the app can use the {es-sql} driver, it can use {es-sql}. From 7dd22f09dcebb44da6e9f6aa22bdd2f7ceb78c9a Mon Sep 17 00:00:00 2001 From: Michael Basnight Date: Thu, 13 Sep 2018 15:17:37 -0500 Subject: [PATCH 16/45] Mute failing JdbcSqlSpec functions Relates #33687 --- .../sql/src/main/resources/string-functions.sql-spec | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec index c0b0430b278..8fe35780443 100644 --- a/x-pack/qa/sql/src/main/resources/string-functions.sql-spec +++ b/x-pack/qa/sql/src/main/resources/string-functions.sql-spec @@ -157,14 +157,16 @@ SELECT SUBSTRING('Elasticsearch', 10, 10) sub; ucaseFilter SELECT UCASE(gender) uppercased, COUNT(*) count FROM "test_emp" WHERE UCASE(gender) = 'F' GROUP BY UCASE(gender); -ucaseInline1 -SELECT UCASE('ElAsTiC') upper; +//https://github.com/elastic/elasticsearch/issues/33687 +//ucaseInline1 +//SELECT UCASE('ElAsTiC') upper; ucaseInline2 SELECT UCASE('') upper; -ucaseInline3 -SELECT UCASE(' elastic ') upper; +//https://github.com/elastic/elasticsearch/issues/33687 +//ucaseInline3 +//SELECT UCASE(' elastic ') upper; // // Group and order by From 3914a980f799b39a0c2983fba9640ae50f8d2e4c Mon Sep 17 00:00:00 2001 From: Jay Modi Date: Thu, 13 Sep 2018 14:40:36 -0600 Subject: [PATCH 17/45] Security: remove wrapping in put user response (#33512) This change removes the wrapping of the created field in the put user response. The created field was added as a top level field in #32332, while also still being wrapped within the `user` object of the response. Since the value is available in both formats in 6.x, we can remove the wrapped version for 7.0. --- docs/reference/migration/migrate_7_0/api.asciidoc | 6 ++++++ x-pack/docs/en/rest-api/security/create-users.asciidoc | 3 --- .../xpack/core/security/action/user/PutUserResponse.java | 8 +++++--- .../security/rest/action/user/RestPutUserAction.java | 7 +------ .../resources/rest-api-spec/test/roles/11_idx_arrays.yml | 2 +- .../test/resources/rest-api-spec/test/users/10_basic.yml | 4 ++-- .../rest-api-spec/test/users/15_overwrite_user.yml | 2 +- .../resources/rest-api-spec/test/users/16_update_user.yml | 4 ++-- .../rest-api-spec/test/remote_cluster/10_basic.yml | 2 +- .../rest-api-spec/test/old_cluster/20_security.yml | 2 +- 10 files changed, 20 insertions(+), 20 deletions(-) diff --git a/docs/reference/migration/migrate_7_0/api.asciidoc b/docs/reference/migration/migrate_7_0/api.asciidoc index ce2d817ac50..a58223023bd 100644 --- a/docs/reference/migration/migrate_7_0/api.asciidoc +++ b/docs/reference/migration/migrate_7_0/api.asciidoc @@ -87,3 +87,9 @@ depending on whether {security} is enabled. Previously a 404 - NOT FOUND (IndexNotFoundException) could be returned in case the current user was not authorized for any alias. An empty response with status 200 - OK is now returned instead at all times. + +==== Put User API response no longer has `user` object + +The Put User API response was changed in 6.5.0 to add the `created` field +outside of the user object where it previously had been. In 7.0.0 the user +object has been removed in favor of the top level `created` field. diff --git a/x-pack/docs/en/rest-api/security/create-users.asciidoc b/x-pack/docs/en/rest-api/security/create-users.asciidoc index 789e8c7e80d..d18618af273 100644 --- a/x-pack/docs/en/rest-api/security/create-users.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-users.asciidoc @@ -90,9 +90,6 @@ created or updated. [source,js] -------------------------------------------------- { - "user": { - "created" : true - }, "created": true <1> } -------------------------------------------------- diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java index 4d0e5fdfa4b..c8958251e16 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/user/PutUserResponse.java @@ -9,7 +9,7 @@ package org.elasticsearch.xpack.core.security.action.user; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; -import org.elasticsearch.common.xcontent.ToXContentFragment; +import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; import java.io.IOException; @@ -18,7 +18,7 @@ import java.io.IOException; * Response when adding a user to the security index. Returns a * single boolean field for whether the user was created or updated. */ -public class PutUserResponse extends ActionResponse implements ToXContentFragment { +public class PutUserResponse extends ActionResponse implements ToXContentObject { private boolean created; @@ -47,6 +47,8 @@ public class PutUserResponse extends ActionResponse implements ToXContentFragmen @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - return builder.field("created", created); + return builder.startObject() + .field("created", created) + .endObject(); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java index d6fc6aae381..521ab76c96b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/user/RestPutUserAction.java @@ -58,13 +58,8 @@ public class RestPutUserAction extends SecurityBaseRestHandler implements RestRe return channel -> requestBuilder.execute(new RestBuilderListener(channel) { @Override public RestResponse buildResponse(PutUserResponse putUserResponse, XContentBuilder builder) throws Exception { - builder.startObject() - .startObject("user"); // TODO in 7.0 remove wrapping of response in the user object and just return the object putUserResponse.toXContent(builder, request); - builder.endObject(); - - putUserResponse.toXContent(builder, request); - return new BytesRestResponse(RestStatus.OK, builder.endObject()); + return new BytesRestResponse(RestStatus.OK, builder); } }); } diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml index 4d32866ee8b..84e2ae4d412 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/roles/11_idx_arrays.yml @@ -51,7 +51,7 @@ teardown: "password": "s3krit", "roles" : [ "admin_role2" ] } - - match: { user: { created: true } } + - match: { created: true } - do: index: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml index 5e5138c88fb..ea152bd677c 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/10_basic.yml @@ -30,7 +30,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: true } } + - match: { created: true } - do: headers: @@ -65,7 +65,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: true } } + - match: { created: true } - do: headers: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml index 66bcc9d1c5a..efe4d4e4c92 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/15_overwrite_user.yml @@ -51,7 +51,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: false } } + - match: { created: false } - do: xpack.security.get_user: diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml index abe6f44369a..2a477e8bfbb 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/users/16_update_user.yml @@ -66,7 +66,7 @@ teardown: "key2" : "val2" } } - - match: { user: { created: false } } + - match: { created: false } # validate existing password works - do: @@ -103,7 +103,7 @@ teardown: "key3" : "val3" } } - - match: { user: { created: false } } + - match: { created: false } # validate old password doesn't work - do: diff --git a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml index 6fa2b1e31a1..adcf0cf0770 100644 --- a/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml +++ b/x-pack/qa/multi-cluster-search-security/src/test/resources/rest-api-spec/test/remote_cluster/10_basic.yml @@ -195,4 +195,4 @@ setup: "password": "s3krit", "roles" : [ ] } - - match: { user: { created: false } } + - match: { created: false } diff --git a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml index 119f6f48749..c145d439424 100644 --- a/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml +++ b/x-pack/qa/rolling-upgrade/src/test/resources/rest-api-spec/test/old_cluster/20_security.yml @@ -9,7 +9,7 @@ "password" : "x-pack-test-password", "roles" : [ "native_role" ] } - - match: { user: { created: true } } + - match: { created: true } - do: xpack.security.put_role: From d3e27ff2f62371cd93d14f61cd8a1bd881129f99 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 14 Sep 2018 10:07:19 +1000 Subject: [PATCH 18/45] [Kerberos] Move tests based on SimpleKdc to evil-tests (#33492) We have a test dependency on Apache Mina when using SimpleKdcServer for testing Kerberos. When checking for LDAP backend connectivity, the code checks for deadlocks which require additional security permissions accessClassInPackage.sun.reflect. As this is only for test and we do not want to add security permissions to production, this commit moves these tests and related classes to x-pack evil-tests where they can run with security manager disabled. The plan is to handle the security manager exception in the upstream issue DIRMINA-1093 and then once the release is available to run these tests with security manager enabled. Closes #32739 --- .../kerberos/KerberosRealmCacheTests.java | 4 +- .../kerberos/KerberosRealmSettingsTests.java | 4 +- .../authc/kerberos/KerberosRealmTestCase.java | 52 ++++++++++++++++- .../authc/kerberos/KerberosRealmTests.java | 2 +- x-pack/qa/evil-tests/build.gradle | 4 +- .../authc/kerberos/KerberosTestCase.java | 56 ++----------------- .../KerberosTicketValidatorTests.java | 4 +- .../authc/kerberos/SimpleKdcLdapServer.java | 0 .../kerberos/SimpleKdcLdapServerTests.java | 0 .../security/authc/kerberos/SpnegoClient.java | 0 .../evil-tests}/src/test/resources/kdc.ldiff | 0 11 files changed, 65 insertions(+), 61 deletions(-) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java (73%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java (96%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java (100%) rename x-pack/{plugin/security => qa/evil-tests}/src/test/resources/kdc.ldiff (100%) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index 69ebe15c5d7..ee2e2675e18 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -102,8 +102,8 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { public void testAuthenticateWithValidTicketSucessAuthnWithUserDetailsWhenCacheDisabled() throws LoginException, GSSException, IOException { // if cache.ttl <= 0 then the cache is disabled - settings = KerberosTestCase.buildKerberosRealmSettings( - KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true, + settings = buildKerberosRealmSettings( + writeKeyTab(dir.resolve("key.keytab"), randomAlphaOfLength(4)).toString(), 100, "0m", true, randomBoolean()); final String username = randomPrincipalName(); final String outToken = randomAlphaOfLength(10); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java index 2e47d03d49d..55687d51888 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmSettingsTests.java @@ -27,12 +27,12 @@ public class KerberosRealmSettingsTests extends ESTestCase { configDir = Files.createDirectory(configDir); } final String keytabPathConfig = "config" + dir.getFileSystem().getSeparator() + "http.keytab"; - KerberosTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); + KerberosRealmTestCase.writeKeyTab(dir.resolve(keytabPathConfig), null); final Integer maxUsers = randomInt(); final String cacheTTL = randomLongBetween(10L, 100L) + "m"; final boolean enableDebugLogs = randomBoolean(); final boolean removeRealmName = randomBoolean(); - final Settings settings = KerberosTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, enableDebugLogs, + final Settings settings = KerberosRealmTestCase.buildKerberosRealmSettings(keytabPathConfig, maxUsers, cacheTTL, enableDebugLogs, removeRealmName); assertThat(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings), equalTo(keytabPathConfig)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index dd83da49a0b..8f959a26bb8 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.authc.kerberos; import org.elasticsearch.action.ActionListener; import org.elasticsearch.client.Client; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; @@ -30,6 +31,10 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.After; import org.junit.Before; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.Collections; @@ -71,7 +76,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase { resourceWatcherService = new ResourceWatcherService(Settings.EMPTY, threadPool); dir = createTempDir(); globalSettings = Settings.builder().put("path.home", dir).build(); - settings = KerberosTestCase.buildKerberosRealmSettings(KerberosTestCase.writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), + settings = buildKerberosRealmSettings(writeKeyTab(dir.resolve("key.keytab"), "asa").toString(), 100, "10m", true, randomBoolean()); licenseState = mock(XPackLicenseState.class); when(licenseState.isAuthorizationRealmAllowed()).thenReturn(true); @@ -177,4 +182,49 @@ public abstract class KerberosRealmTestCase extends ESTestCase { } return principalName; } + + /** + * Write content to provided keytab file. + * + * @param keytabPath {@link Path} to keytab file. + * @param content Content for keytab + * @return key tab path + * @throws IOException if I/O error occurs while writing keytab file + */ + public static Path writeKeyTab(final Path keytabPath, final String content) throws IOException { + try (BufferedWriter bufferedWriter = Files.newBufferedWriter(keytabPath, StandardCharsets.US_ASCII)) { + bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); + } + return keytabPath; + } + + /** + * Build kerberos realm settings with default config and given keytab + * + * @param keytabPath key tab file path + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath) { + return buildKerberosRealmSettings(keytabPath, 100, "10m", true, false); + } + + /** + * Build kerberos realm settings + * + * @param keytabPath key tab file path + * @param maxUsersInCache max users to be maintained in cache + * @param cacheTTL time to live for cached entries + * @param enableDebugging for krb5 logs + * @param removeRealmName {@code true} if we want to remove realm name from the username of form 'user@REALM' + * @return {@link Settings} for kerberos realm + */ + public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, + final boolean enableDebugging, final boolean removeRealmName) { + final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) + .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) + .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) + .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging) + .put(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.getKey(), removeRealmName); + return builder.build(); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java index d35068fd07a..1166e929341 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -155,7 +155,7 @@ public class KerberosRealmTests extends KerberosRealmTestCase { } private void assertKerberosRealmConstructorFails(final String keytabPath, final String expectedErrorMessage) { - settings = KerberosTestCase.buildKerberosRealmSettings(keytabPath, 100, "10m", true, randomBoolean()); + settings = buildKerberosRealmSettings(keytabPath, 100, "10m", true, randomBoolean()); config = new RealmConfig("test-kerb-realm", settings, globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings)); mockNativeRoleMappingStore = roleMappingStore(Arrays.asList("user")); diff --git a/x-pack/qa/evil-tests/build.gradle b/x-pack/qa/evil-tests/build.gradle index 03f2a569873..d411909fb31 100644 --- a/x-pack/qa/evil-tests/build.gradle +++ b/x-pack/qa/evil-tests/build.gradle @@ -1,9 +1,11 @@ apply plugin: 'elasticsearch.standalone-test' dependencies { - testCompile "org.elasticsearch.plugin:x-pack-core:${version}" + testCompile project(path: xpackModule('core'), configuration: 'testArtifacts') + testCompile project(path: xpackModule('security'), configuration: 'testArtifacts') } test { systemProperty 'tests.security.manager', 'false' + include '**/*Tests.class' } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java similarity index 73% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java index f97afc1d52c..f8795e6b4da 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTestCase.java @@ -9,20 +9,15 @@ package org.elasticsearch.xpack.security.authc.kerberos; import org.apache.logging.log4j.Logger; import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.common.Randomness; -import org.elasticsearch.common.Strings; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.security.authc.kerberos.KerberosRealmSettings; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; -import java.io.BufferedWriter; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; import java.nio.file.Path; import java.security.AccessController; import java.security.PrivilegedActionException; @@ -130,12 +125,14 @@ public abstract class KerberosTestCase extends ESTestCase { throw ExceptionsHelper.convertToRuntime(e); } }); - settings = buildKerberosRealmSettings(ktabPathForService.toString()); + settings = KerberosRealmTestCase.buildKerberosRealmSettings(ktabPathForService.toString()); } @After public void tearDownMiniKdc() throws IOException, PrivilegedActionException { - simpleKdcLdapServer.stop(); + if (simpleKdcLdapServer != null) { + simpleKdcLdapServer.stop(); + } } /** @@ -186,49 +183,4 @@ public abstract class KerberosTestCase extends ESTestCase { return AccessController.doPrivileged((PrivilegedExceptionAction) () -> Subject.doAs(subject, action)); } - /** - * Write content to provided keytab file. - * - * @param keytabPath {@link Path} to keytab file. - * @param content Content for keytab - * @return key tab path - * @throws IOException if I/O error occurs while writing keytab file - */ - public static Path writeKeyTab(final Path keytabPath, final String content) throws IOException { - try (BufferedWriter bufferedWriter = Files.newBufferedWriter(keytabPath, StandardCharsets.US_ASCII)) { - bufferedWriter.write(Strings.isNullOrEmpty(content) ? "test-content" : content); - } - return keytabPath; - } - - /** - * Build kerberos realm settings with default config and given keytab - * - * @param keytabPath key tab file path - * @return {@link Settings} for kerberos realm - */ - public static Settings buildKerberosRealmSettings(final String keytabPath) { - return buildKerberosRealmSettings(keytabPath, 100, "10m", true, false); - } - - /** - * Build kerberos realm settings - * - * @param keytabPath key tab file path - * @param maxUsersInCache max users to be maintained in cache - * @param cacheTTL time to live for cached entries - * @param enableDebugging for krb5 logs - * @param removeRealmName {@code true} if we want to remove realm name from the username of form 'user@REALM' - * @return {@link Settings} for kerberos realm - */ - public static Settings buildKerberosRealmSettings(final String keytabPath, final int maxUsersInCache, final String cacheTTL, - final boolean enableDebugging, final boolean removeRealmName) { - final Settings.Builder builder = Settings.builder().put(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.getKey(), keytabPath) - .put(KerberosRealmSettings.CACHE_MAX_USERS_SETTING.getKey(), maxUsersInCache) - .put(KerberosRealmSettings.CACHE_TTL_SETTING.getKey(), cacheTTL) - .put(KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.getKey(), enableDebugging) - .put(KerberosRealmSettings.SETTING_REMOVE_REALM_NAME.getKey(), removeRealmName); - return builder.build(); - } - } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java similarity index 96% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java index 8f35e0bde44..340d05ce35e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java +++ b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosTicketValidatorTests.java @@ -86,8 +86,8 @@ public class KerberosTicketValidatorTests extends KerberosTestCase { final String base64KerbToken = spnegoClient.getBase64EncodedTokenForSpnegoHeader(); assertThat(base64KerbToken, is(notNullValue())); - final Path ktabPath = writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); - settings = buildKerberosRealmSettings(ktabPath.toString()); + final Path ktabPath = KerberosRealmTestCase.writeKeyTab(workDir.resolve("invalid.keytab"), "not - a - valid - key - tab"); + settings = KerberosRealmTestCase.buildKerberosRealmSettings(ktabPath.toString()); final Environment env = TestEnvironment.newEnvironment(globalSettings); final Path keytabPath = env.configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(settings)); final PlainActionFuture> future = new PlainActionFuture<>(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServer.java diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SimpleKdcLdapServerTests.java diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java b/x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java similarity index 100% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java rename to x-pack/qa/evil-tests/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/SpnegoClient.java diff --git a/x-pack/plugin/security/src/test/resources/kdc.ldiff b/x-pack/qa/evil-tests/src/test/resources/kdc.ldiff similarity index 100% rename from x-pack/plugin/security/src/test/resources/kdc.ldiff rename to x-pack/qa/evil-tests/src/test/resources/kdc.ldiff From 189aaceecf764d5ec7b34cd26e27ae6f621fe4cf Mon Sep 17 00:00:00 2001 From: Nhat Nguyen Date: Thu, 13 Sep 2018 22:15:21 -0400 Subject: [PATCH 19/45] AwaitsFix testRestoreMinmal Tracked at #33689 --- .../elasticsearch/snapshots/SourceOnlySnapshotShardTests.java | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java index 7058724ecf0..261133b8907 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/snapshots/SourceOnlySnapshotShardTests.java @@ -162,6 +162,7 @@ public class SourceOnlySnapshotShardTests extends IndexShardTestCase { return "{ \"value\" : \"" + randomAlphaOfLength(10) + "\"}"; } + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33689") public void testRestoreMinmal() throws IOException { IndexShard shard = newStartedShard(true); int numInitialDocs = randomIntBetween(10, 100); From 0b4960ff6b93d94dae824156dddbf0084f8ea1b3 Mon Sep 17 00:00:00 2001 From: Armin Braun Date: Fri, 14 Sep 2018 06:21:18 +0200 Subject: [PATCH 20/45] SCRIPTING: Move terms_set Context to its Own Class (#33602) * SCRIPTING: Move terms_set Context to its Own Class * Extracted TermsSetQueryScript * Kept mechanics close to what they were with SearchScript --- .../index/query/TermsSetQueryBuilder.java | 21 ++-- .../elasticsearch/script/ParameterMap.java | 105 ++++++++++++++++ .../elasticsearch/script/ScriptModule.java | 2 +- .../elasticsearch/script/SearchScript.java | 2 - .../script/TermsSetQueryScript.java | 112 ++++++++++++++++++ .../script/MockScriptEngine.java | 12 ++ 6 files changed, 240 insertions(+), 14 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/script/ParameterMap.java create mode 100644 server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java diff --git a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java index c20df00a109..1e151896df0 100644 --- a/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java +++ b/server/src/main/java/org/elasticsearch/index/query/TermsSetQueryBuilder.java @@ -40,7 +40,6 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.index.fielddata.IndexNumericFieldData; import org.elasticsearch.index.mapper.MappedFieldType; import org.elasticsearch.script.Script; -import org.elasticsearch.script.SearchScript; import java.io.IOException; import java.util.ArrayList; @@ -48,6 +47,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import org.elasticsearch.script.TermsSetQueryScript; public final class TermsSetQueryBuilder extends AbstractQueryBuilder { @@ -262,13 +262,12 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder params = new HashMap<>(); params.putAll(minimumShouldMatchScript.getParams()); params.put("num_terms", values.size()); - SearchScript.LeafFactory leafFactory = factory.newFactory(params, context.lookup()); - longValuesSource = new ScriptLongValueSource(minimumShouldMatchScript, leafFactory); + longValuesSource = new ScriptLongValueSource(minimumShouldMatchScript, factory.newFactory(params, context.lookup())); } else { throw new IllegalStateException("No minimum should match has been specified"); } @@ -278,26 +277,26 @@ public final class TermsSetQueryBuilder extends AbstractQueryBuilder { + + private static final DeprecationLogger DEPRECATION_LOGGER = + new DeprecationLogger(LogManager.getLogger(ParameterMap.class)); + + private final Map params; + + private final Map deprecations; + + ParameterMap(Map params, Map deprecations) { + this.params = params; + this.deprecations = deprecations; + } + + @Override + public int size() { + return params.size(); + } + + @Override + public boolean isEmpty() { + return params.isEmpty(); + } + + @Override + public boolean containsKey(final Object key) { + return params.containsKey(key); + } + + @Override + public boolean containsValue(final Object value) { + return params.containsValue(value); + } + + @Override + public Object get(final Object key) { + String deprecationMessage = deprecations.get(key); + if (deprecationMessage != null) { + DEPRECATION_LOGGER.deprecated(deprecationMessage); + } + return params.get(key); + } + + @Override + public Object put(final String key, final Object value) { + return params.put(key, value); + } + + @Override + public Object remove(final Object key) { + return params.remove(key); + } + + @Override + public void putAll(final Map m) { + params.putAll(m); + } + + @Override + public void clear() { + params.clear(); + } + + @Override + public Set keySet() { + return params.keySet(); + } + + @Override + public Collection values() { + return params.values(); + } + + @Override + public Set> entrySet() { + return params.entrySet(); + } +} diff --git a/server/src/main/java/org/elasticsearch/script/ScriptModule.java b/server/src/main/java/org/elasticsearch/script/ScriptModule.java index 6dc507fa0d8..968bc143ba8 100644 --- a/server/src/main/java/org/elasticsearch/script/ScriptModule.java +++ b/server/src/main/java/org/elasticsearch/script/ScriptModule.java @@ -44,7 +44,7 @@ public class ScriptModule { SearchScript.AGGS_CONTEXT, ScoreScript.CONTEXT, SearchScript.SCRIPT_SORT_CONTEXT, - SearchScript.TERMS_SET_QUERY_CONTEXT, + TermsSetQueryScript.CONTEXT, ExecutableScript.CONTEXT, UpdateScript.CONTEXT, BucketAggregationScript.CONTEXT, diff --git a/server/src/main/java/org/elasticsearch/script/SearchScript.java b/server/src/main/java/org/elasticsearch/script/SearchScript.java index fb5f950d61d..cdf5c98ec62 100644 --- a/server/src/main/java/org/elasticsearch/script/SearchScript.java +++ b/server/src/main/java/org/elasticsearch/script/SearchScript.java @@ -149,6 +149,4 @@ public abstract class SearchScript implements ScorerAware, ExecutableScript { public static final ScriptContext AGGS_CONTEXT = new ScriptContext<>("aggs", Factory.class); // Can return a double. (For ScriptSortType#NUMBER only, for ScriptSortType#STRING normal CONTEXT should be used) public static final ScriptContext SCRIPT_SORT_CONTEXT = new ScriptContext<>("sort", Factory.class); - // Can return a long - public static final ScriptContext TERMS_SET_QUERY_CONTEXT = new ScriptContext<>("terms_set", Factory.class); } diff --git a/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java new file mode 100644 index 00000000000..085f40e0d7a --- /dev/null +++ b/server/src/main/java/org/elasticsearch/script/TermsSetQueryScript.java @@ -0,0 +1,112 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.script; + +import java.io.IOException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.apache.lucene.index.LeafReaderContext; +import org.elasticsearch.index.fielddata.ScriptDocValues; +import org.elasticsearch.search.lookup.LeafSearchLookup; +import org.elasticsearch.search.lookup.SearchLookup; + +public abstract class TermsSetQueryScript { + + public static final String[] PARAMETERS = {}; + + public static final ScriptContext CONTEXT = new ScriptContext<>("terms_set", Factory.class); + + private static final Map DEPRECATIONS; + + static { + Map deprecations = new HashMap<>(); + deprecations.put( + "doc", + "Accessing variable [doc] via [params.doc] from within a terms-set-query-script " + + "is deprecated in favor of directly accessing [doc]." + ); + deprecations.put( + "_doc", + "Accessing variable [doc] via [params._doc] from within a terms-set-query-script " + + "is deprecated in favor of directly accessing [doc]." + ); + DEPRECATIONS = Collections.unmodifiableMap(deprecations); + } + + /** + * The generic runtime parameters for the script. + */ + private final Map params; + + /** + * A leaf lookup for the bound segment this script will operate on. + */ + private final LeafSearchLookup leafLookup; + + public TermsSetQueryScript(Map params, SearchLookup lookup, LeafReaderContext leafContext) { + this.params = new ParameterMap(params, DEPRECATIONS); + this.leafLookup = lookup.getLeafSearchLookup(leafContext); + } + + /** + * Return the parameters for this script. + */ + public Map getParams() { + this.params.putAll(leafLookup.asMap()); + return params; + } + + /** + * The doc lookup for the Lucene segment this script was created for. + */ + public Map> getDoc() { + return leafLookup.doc(); + } + + /** + * Set the current document to run the script on next. + */ + public void setDocument(int docid) { + leafLookup.setDocument(docid); + } + + /** + * Return the result as a long. This is used by aggregation scripts over long fields. + */ + public long runAsLong() { + return execute().longValue(); + } + + public abstract Number execute(); + + /** + * A factory to construct {@link TermsSetQueryScript} instances. + */ + public interface LeafFactory { + TermsSetQueryScript newInstance(LeafReaderContext ctx) throws IOException; + } + + /** + * A factory to construct stateful {@link TermsSetQueryScript} factories for a specific index. + */ + public interface Factory { + LeafFactory newFactory(Map params, SearchLookup lookup); + } +} diff --git a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java index be77846b2ba..be38ae95a32 100644 --- a/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java +++ b/test/framework/src/main/java/org/elasticsearch/script/MockScriptEngine.java @@ -85,6 +85,18 @@ public class MockScriptEngine implements ScriptEngine { if (context.instanceClazz.equals(SearchScript.class)) { SearchScript.Factory factory = mockCompiled::createSearchScript; return context.factoryClazz.cast(factory); + } else if(context.instanceClazz.equals(TermsSetQueryScript.class)) { + TermsSetQueryScript.Factory factory = (parameters, lookup) -> (TermsSetQueryScript.LeafFactory) ctx + -> new TermsSetQueryScript(parameters, lookup, ctx) { + @Override + public Number execute() { + Map vars = new HashMap<>(parameters); + vars.put("params", parameters); + vars.put("doc", getDoc()); + return (Number) script.apply(vars); + } + }; + return context.factoryClazz.cast(factory); } else if (context.instanceClazz.equals(ExecutableScript.class)) { ExecutableScript.Factory factory = mockCompiled::createExecutableScript; return context.factoryClazz.cast(factory); From 736053c6586fa4864e65423df06df25ce8b6949c Mon Sep 17 00:00:00 2001 From: Costin Leau Date: Fri, 14 Sep 2018 09:05:34 +0300 Subject: [PATCH 21/45] SQL: Return functions in JDBC driver metadata (#33672) Update JDBC database metadata to advertised the supported functions Add aliases to some date/time functions to match the ODBC spec Fix #33671 --- .../sql/jdbc/jdbc/JdbcDatabaseMetaData.java | 38 +++- .../expression/function/FunctionRegistry.java | 8 +- .../xpack/qa/sql/cli/ShowTestCase.java | 3 + .../sql/src/main/resources/command.csv-spec | 199 +++++++++--------- .../qa/sql/src/main/resources/docs.csv-spec | 186 ++++++++-------- 5 files changed, 239 insertions(+), 195 deletions(-) diff --git a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java index d2e24f3edac..085016bc0bd 100644 --- a/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java +++ b/x-pack/plugin/sql/jdbc/src/main/java/org/elasticsearch/xpack/sql/jdbc/jdbc/JdbcDatabaseMetaData.java @@ -179,14 +179,34 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getNumericFunctions() throws SQLException { - // TODO: sync this with the grammar - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/numeric-functions?view=sql-server-2017 + return "ABS,ACOS,ASIN,ATAN,ATAN2," + + "CEILING,COS," + + "DEGREES," + + "EXP," + + "FLOOR," + + "LOG,LOG10," + + "MOD," + + "PI,POWER," + + "RADIANS,RAND,ROUND," + + "SIGN,SIN,SQRT," + + "TAN"; } @Override public String getStringFunctions() throws SQLException { - // TODO: sync this with the grammar - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/string-functions?view=sql-server-2017 + return "ASCII," + + "BIT_LENGTH," + + "CHAR,CHAR_LENGTH,CHARACTER_LENGTH,CONCAT," + + "INSERT," + + "LCASE,LEFT,LENGTH,LOCATE,LTRIM," + // waiting on https://github.com/elastic/elasticsearch/issues/33477 + //+ "OCTET_LENGTH," + + "POSITION," + + "REPEAT,REPLACE,RIGHT,RTRIM," + + "SPACE,SUBSTRING," + + "UCASE"; } @Override @@ -197,7 +217,15 @@ class JdbcDatabaseMetaData implements DatabaseMetaData, JdbcWrapper { @Override public String getTimeDateFunctions() throws SQLException { - return ""; + //https://docs.microsoft.com/en-us/sql/odbc/reference/appendixes/time-date-and-interval-functions?view=sql-server-2017 + return "DAYNAME,DAYOFMONTH,DAYOFWEEK,DAYOFYEAR" + + "EXTRACT," + + "HOUR," + + "MINUTE,MONTH,MONTHNAME" + + "QUARTER," + + "SECOND," + + "WEEK," + + "YEAR"; } @Override diff --git a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java index 820aafb0116..2daa90c7bda 100644 --- a/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java +++ b/x-pack/plugin/sql/src/main/java/org/elasticsearch/xpack/sql/expression/function/FunctionRegistry.java @@ -116,9 +116,9 @@ public class FunctionRegistry { def(Kurtosis.class, Kurtosis::new), // Scalar functions // Date - def(DayOfMonth.class, DayOfMonth::new, "DAY", "DOM"), - def(DayOfWeek.class, DayOfWeek::new, "DOW"), - def(DayOfYear.class, DayOfYear::new, "DOY"), + def(DayOfMonth.class, DayOfMonth::new, "DAYOFMONTH", "DAY", "DOM"), + def(DayOfWeek.class, DayOfWeek::new, "DAYOFWEEK", "DOW"), + def(DayOfYear.class, DayOfYear::new, "DAYOFYEAR", "DOY"), def(HourOfDay.class, HourOfDay::new, "HOUR"), def(MinuteOfDay.class, MinuteOfDay::new), def(MinuteOfHour.class, MinuteOfHour::new, "MINUTE"), @@ -163,7 +163,7 @@ public class FunctionRegistry { def(Ascii.class, Ascii::new), def(Char.class, Char::new), def(BitLength.class, BitLength::new), - def(CharLength.class, CharLength::new), + def(CharLength.class, CharLength::new, "CHARACTER_LENGTH"), def(LCase.class, LCase::new), def(Length.class, Length::new), def(LTrim.class, LTrim::new), diff --git a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java index 601dca8abd4..8dbd4b187f7 100644 --- a/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java +++ b/x-pack/qa/sql/src/main/java/org/elasticsearch/xpack/qa/sql/cli/ShowTestCase.java @@ -60,9 +60,12 @@ public abstract class ShowTestCase extends CliIntegrationTestCase { assertThat(command("SHOW FUNCTIONS LIKE '%DAY%'"), RegexMatcher.matches("\\s*name\\s*\\|\\s*type\\s*")); assertThat(readLine(), containsString("----------")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_MONTH\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFMONTH\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_WEEK\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFWEEK\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_OF_YEAR\\s*\\|\\s*SCALAR\\s*")); + assertThat(readLine(), RegexMatcher.matches("\\s*DAYOFYEAR\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*HOUR_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*MINUTE_OF_DAY\\s*\\|\\s*SCALAR\\s*")); assertThat(readLine(), RegexMatcher.matches("\\s*DAY_NAME\\s*\\|\\s*SCALAR\\s*")); diff --git a/x-pack/qa/sql/src/main/resources/command.csv-spec b/x-pack/qa/sql/src/main/resources/command.csv-spec index 28aadeded2c..81aa18b2e84 100644 --- a/x-pack/qa/sql/src/main/resources/command.csv-spec +++ b/x-pack/qa/sql/src/main/resources/command.csv-spec @@ -7,93 +7,97 @@ showFunctions SHOW FUNCTIONS; name:s | type:s -AVG |AGGREGATE -COUNT |AGGREGATE -MAX |AGGREGATE -MIN |AGGREGATE -SUM |AGGREGATE -STDDEV_POP |AGGREGATE -VAR_POP |AGGREGATE -PERCENTILE |AGGREGATE -PERCENTILE_RANK |AGGREGATE -SUM_OF_SQUARES |AGGREGATE -SKEWNESS |AGGREGATE -KURTOSIS |AGGREGATE -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DOM |SCALAR -DAY_OF_WEEK |SCALAR -DOW |SCALAR -DAY_OF_YEAR |SCALAR -DOY |SCALAR -HOUR_OF_DAY |SCALAR -HOUR |SCALAR -MINUTE_OF_DAY |SCALAR -MINUTE_OF_HOUR |SCALAR -MINUTE |SCALAR -SECOND_OF_MINUTE|SCALAR -SECOND |SCALAR -MONTH_OF_YEAR |SCALAR -MONTH |SCALAR -YEAR |SCALAR -WEEK_OF_YEAR |SCALAR -WEEK |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR -MONTH_NAME |SCALAR -MONTHNAME |SCALAR -QUARTER |SCALAR -ABS |SCALAR -ACOS |SCALAR -ASIN |SCALAR -ATAN |SCALAR -ATAN2 |SCALAR -CBRT |SCALAR -CEIL |SCALAR -CEILING |SCALAR -COS |SCALAR -COSH |SCALAR -COT |SCALAR -DEGREES |SCALAR -E |SCALAR -EXP |SCALAR -EXPM1 |SCALAR -FLOOR |SCALAR -LOG |SCALAR -LOG10 |SCALAR -MOD |SCALAR -PI |SCALAR -POWER |SCALAR -RADIANS |SCALAR -RANDOM |SCALAR -RAND |SCALAR -ROUND |SCALAR -SIGN |SCALAR -SIGNUM |SCALAR -SIN |SCALAR -SINH |SCALAR -SQRT |SCALAR -TAN |SCALAR -ASCII |SCALAR -CHAR |SCALAR -BIT_LENGTH |SCALAR -CHAR_LENGTH |SCALAR -LCASE |SCALAR -LENGTH |SCALAR -LTRIM |SCALAR -RTRIM |SCALAR -SPACE |SCALAR -CONCAT |SCALAR -INSERT |SCALAR -LEFT |SCALAR -LOCATE |SCALAR -POSITION |SCALAR -REPEAT |SCALAR -REPLACE |SCALAR -RIGHT |SCALAR -SUBSTRING |SCALAR -UCASE |SCALAR -SCORE |SCORE +AVG |AGGREGATE +COUNT |AGGREGATE +MAX |AGGREGATE +MIN |AGGREGATE +SUM |AGGREGATE +STDDEV_POP |AGGREGATE +VAR_POP |AGGREGATE +PERCENTILE |AGGREGATE +PERCENTILE_RANK |AGGREGATE +SUM_OF_SQUARES |AGGREGATE +SKEWNESS |AGGREGATE +KURTOSIS |AGGREGATE +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DOM |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DOW |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +DOY |SCALAR +HOUR_OF_DAY |SCALAR +HOUR |SCALAR +MINUTE_OF_DAY |SCALAR +MINUTE_OF_HOUR |SCALAR +MINUTE |SCALAR +SECOND_OF_MINUTE|SCALAR +SECOND |SCALAR +MONTH_OF_YEAR |SCALAR +MONTH |SCALAR +YEAR |SCALAR +WEEK_OF_YEAR |SCALAR +WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR +ABS |SCALAR +ACOS |SCALAR +ASIN |SCALAR +ATAN |SCALAR +ATAN2 |SCALAR +CBRT |SCALAR +CEIL |SCALAR +CEILING |SCALAR +COS |SCALAR +COSH |SCALAR +COT |SCALAR +DEGREES |SCALAR +E |SCALAR +EXP |SCALAR +EXPM1 |SCALAR +FLOOR |SCALAR +LOG |SCALAR +LOG10 |SCALAR +MOD |SCALAR +PI |SCALAR +POWER |SCALAR +RADIANS |SCALAR +RANDOM |SCALAR +RAND |SCALAR +ROUND |SCALAR +SIGN |SCALAR +SIGNUM |SCALAR +SIN |SCALAR +SINH |SCALAR +SQRT |SCALAR +TAN |SCALAR +ASCII |SCALAR +CHAR |SCALAR +BIT_LENGTH |SCALAR +CHAR_LENGTH |SCALAR +CHARACTER_LENGTH|SCALAR +LCASE |SCALAR +LENGTH |SCALAR +LTRIM |SCALAR +RTRIM |SCALAR +SPACE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR +UCASE |SCALAR +SCORE |SCORE ; showFunctionsWithExactMatch @@ -128,15 +132,18 @@ ABS |SCALAR showFunctionsWithLeadingPattern SHOW FUNCTIONS LIKE '%DAY%'; - name:s | type:s -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DAY_OF_WEEK |SCALAR -DAY_OF_YEAR |SCALAR -HOUR_OF_DAY |SCALAR -MINUTE_OF_DAY |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR + name:s | type:s +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +HOUR_OF_DAY |SCALAR +MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR ; showTables diff --git a/x-pack/qa/sql/src/main/resources/docs.csv-spec b/x-pack/qa/sql/src/main/resources/docs.csv-spec index 52356bdfd52..280e9a5edf0 100644 --- a/x-pack/qa/sql/src/main/resources/docs.csv-spec +++ b/x-pack/qa/sql/src/main/resources/docs.csv-spec @@ -183,94 +183,97 @@ SHOW FUNCTIONS; name | type ----------------+--------------- -AVG |AGGREGATE -COUNT |AGGREGATE -MAX |AGGREGATE -MIN |AGGREGATE -SUM |AGGREGATE -STDDEV_POP |AGGREGATE -VAR_POP |AGGREGATE -PERCENTILE |AGGREGATE -PERCENTILE_RANK |AGGREGATE -SUM_OF_SQUARES |AGGREGATE -SKEWNESS |AGGREGATE -KURTOSIS |AGGREGATE -DAY_OF_MONTH |SCALAR -DAY |SCALAR -DOM |SCALAR -DAY_OF_WEEK |SCALAR -DOW |SCALAR -DAY_OF_YEAR |SCALAR -DOY |SCALAR -HOUR_OF_DAY |SCALAR -HOUR |SCALAR -MINUTE_OF_DAY |SCALAR -MINUTE_OF_HOUR |SCALAR -MINUTE |SCALAR -SECOND_OF_MINUTE|SCALAR -SECOND |SCALAR -MONTH_OF_YEAR |SCALAR -MONTH |SCALAR -YEAR |SCALAR -WEEK_OF_YEAR |SCALAR -WEEK |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR -MONTH_NAME |SCALAR -MONTHNAME |SCALAR -QUARTER |SCALAR -ABS |SCALAR -ACOS |SCALAR -ASIN |SCALAR -ATAN |SCALAR -ATAN2 |SCALAR -CBRT |SCALAR -CEIL |SCALAR -CEILING |SCALAR -COS |SCALAR -COSH |SCALAR -COT |SCALAR -DEGREES |SCALAR -E |SCALAR -EXP |SCALAR -EXPM1 |SCALAR -FLOOR |SCALAR -LOG |SCALAR -LOG10 |SCALAR -MOD |SCALAR -PI |SCALAR -POWER |SCALAR -RADIANS |SCALAR -RANDOM |SCALAR -RAND |SCALAR -ROUND |SCALAR -SIGN |SCALAR -SIGNUM |SCALAR -SIN |SCALAR -SINH |SCALAR -SQRT |SCALAR -TAN |SCALAR -ASCII |SCALAR -CHAR |SCALAR -BIT_LENGTH |SCALAR +AVG |AGGREGATE +COUNT |AGGREGATE +MAX |AGGREGATE +MIN |AGGREGATE +SUM |AGGREGATE +STDDEV_POP |AGGREGATE +VAR_POP |AGGREGATE +PERCENTILE |AGGREGATE +PERCENTILE_RANK |AGGREGATE +SUM_OF_SQUARES |AGGREGATE +SKEWNESS |AGGREGATE +KURTOSIS |AGGREGATE +DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR +DAY |SCALAR +DOM |SCALAR +DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR +DOW |SCALAR +DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR +DOY |SCALAR +HOUR_OF_DAY |SCALAR +HOUR |SCALAR +MINUTE_OF_DAY |SCALAR +MINUTE_OF_HOUR |SCALAR +MINUTE |SCALAR +SECOND_OF_MINUTE|SCALAR +SECOND |SCALAR +MONTH_OF_YEAR |SCALAR +MONTH |SCALAR +YEAR |SCALAR +WEEK_OF_YEAR |SCALAR +WEEK |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR +MONTH_NAME |SCALAR +MONTHNAME |SCALAR +QUARTER |SCALAR +ABS |SCALAR +ACOS |SCALAR +ASIN |SCALAR +ATAN |SCALAR +ATAN2 |SCALAR +CBRT |SCALAR +CEIL |SCALAR +CEILING |SCALAR +COS |SCALAR +COSH |SCALAR +COT |SCALAR +DEGREES |SCALAR +E |SCALAR +EXP |SCALAR +EXPM1 |SCALAR +FLOOR |SCALAR +LOG |SCALAR +LOG10 |SCALAR +MOD |SCALAR +PI |SCALAR +POWER |SCALAR +RADIANS |SCALAR +RANDOM |SCALAR +RAND |SCALAR +ROUND |SCALAR +SIGN |SCALAR +SIGNUM |SCALAR +SIN |SCALAR +SINH |SCALAR +SQRT |SCALAR +TAN |SCALAR +ASCII |SCALAR +CHAR |SCALAR +BIT_LENGTH |SCALAR CHAR_LENGTH |SCALAR -LCASE |SCALAR -LENGTH |SCALAR -LTRIM |SCALAR -RTRIM |SCALAR -SPACE |SCALAR -CONCAT |SCALAR -INSERT |SCALAR -LEFT |SCALAR -LOCATE |SCALAR -POSITION |SCALAR -REPEAT |SCALAR -REPLACE |SCALAR -RIGHT |SCALAR -SUBSTRING |SCALAR -UCASE |SCALAR -SCORE |SCORE - +CHARACTER_LENGTH|SCALAR +LCASE |SCALAR +LENGTH |SCALAR +LTRIM |SCALAR +RTRIM |SCALAR +SPACE |SCALAR +CONCAT |SCALAR +INSERT |SCALAR +LEFT |SCALAR +LOCATE |SCALAR +POSITION |SCALAR +REPEAT |SCALAR +REPLACE |SCALAR +RIGHT |SCALAR +SUBSTRING |SCALAR +UCASE |SCALAR +SCORE |SCORE // end::showFunctions ; @@ -319,13 +322,16 @@ SHOW FUNCTIONS LIKE '%DAY%'; name | type ---------------+--------------- DAY_OF_MONTH |SCALAR +DAYOFMONTH |SCALAR DAY |SCALAR DAY_OF_WEEK |SCALAR +DAYOFWEEK |SCALAR DAY_OF_YEAR |SCALAR +DAYOFYEAR |SCALAR HOUR_OF_DAY |SCALAR -MINUTE_OF_DAY |SCALAR -DAY_NAME |SCALAR -DAYNAME |SCALAR +MINUTE_OF_DAY |SCALAR +DAY_NAME |SCALAR +DAYNAME |SCALAR // end::showFunctionsWithPattern ; From 8ae1eeb3039efae9048e92362bfbde91c95d48d4 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 14 Sep 2018 09:42:03 +0300 Subject: [PATCH 22/45] [TESTS] Disable specific locales for RestrictedTrustManagerTest (#33299) Disable specific Thai and Japanese locales as Certificate expiration validation fails due to the date parsing of BouncyCastle (that manifests in a FIPS 140 JVM as this is the only place we use BouncyCastle). Added the locale switching logic here instead of subclassing ESTestCase as these are the only tests that fail for these locales and JVM combination. Resolves #33081 --- .../core/ssl/RestrictedTrustManagerTests.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java index c1a39582e4f..24dc2d9847a 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/RestrictedTrustManagerTests.java @@ -5,12 +5,17 @@ */ package org.elasticsearch.xpack.core.ssl; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.test.ESTestCase; import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; +import org.junit.AfterClass; import org.junit.Assert; import org.junit.Before; +import org.junit.BeforeClass; + import javax.net.ssl.X509ExtendedTrustManager; import java.io.IOException; @@ -28,6 +33,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.Objects; import java.util.regex.Pattern; @@ -40,6 +46,34 @@ public class RestrictedTrustManagerTests extends ESTestCase { private int numberOfClusters; private int numberOfNodes; + private static Locale restoreLocale; + + @BeforeClass + public static void ensureSupportedLocale() throws Exception { + Logger logger = Loggers.getLogger(RestrictedTrustManagerTests.class); + if (isUnusableLocale()) { + // See: https://github.com/elastic/elasticsearch/issues/33081 + logger.warn("Attempting to run RestrictedTrustManagerTests tests in an unusable locale in a FIPS JVM. Certificate expiration " + + "validation will fail, switching to English"); + restoreLocale = Locale.getDefault(); + Locale.setDefault(Locale.ENGLISH); + } + } + + private static boolean isUnusableLocale() { + return inFipsJvm() && (Locale.getDefault().toLanguageTag().equals("th-TH") + || Locale.getDefault().toLanguageTag().equals("ja-JP-u-ca-japanese-x-lvariant-JP") + || Locale.getDefault().toLanguageTag().equals("th-TH-u-nu-thai-x-lvariant-TH")); + } + + @AfterClass + public static void restoreLocale() throws Exception { + if (restoreLocale != null) { + Locale.setDefault(restoreLocale); + restoreLocale = null; + } + } + @Before public void readCertificates() throws GeneralSecurityException, IOException { From d810f1b094e7629332d282ba3e403187ab7b17b3 Mon Sep 17 00:00:00 2001 From: Yogesh Gaikwad <902768+bizybot@users.noreply.github.com> Date: Fri, 14 Sep 2018 17:17:53 +1000 Subject: [PATCH 23/45] [Kerberos] Add realm name & UPN to user metadata (#33338) We have a Kerberos setting to remove realm part from the user principal name (remove_realm_name). If this is true then the realm name is removed to form username but in the process, the realm name is lost. For scenarios like Kerberos cross-realm authentication, one could make use of the realm name to determine role mapping for users coming from different realms. This commit adds user metadata for kerberos_realm and kerberos_user_principal_name. --- .../configuring-kerberos-realm.asciidoc | 6 +++ .../authc/kerberos/KerberosRealm.java | 48 +++++++++---------- .../KerberosRealmAuthenticateFailedTests.java | 7 ++- .../kerberos/KerberosRealmCacheTests.java | 17 +++++-- .../authc/kerberos/KerberosRealmTestCase.java | 14 ++++++ .../authc/kerberos/KerberosRealmTests.java | 7 ++- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc index 9e7ed476272..cc0863112c7 100644 --- a/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc +++ b/x-pack/docs/en/security/authentication/configuring-kerberos-realm.asciidoc @@ -165,6 +165,12 @@ POST _xpack/security/role_mapping/kerbrolemapping -------------------------------------------------- // CONSOLE +In case you want to support Kerberos cross realm authentication you may +need to map roles based on the Kerberos realm name. For such scenarios +following are the additional user metadata available for role mapping: +- `kerberos_realm` will be set to Kerberos realm name. +- `kerberos_user_principal_name` will be set to user principal name from the Kerberos ticket. + For more information, see {stack-ov}/mapping-roles.html[Mapping users and groups to roles]. NOTE: The Kerberos realm supports diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java index 9c531d3159f..0f47b6032f5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealm.java @@ -30,6 +30,7 @@ import org.ietf.jgss.GSSException; import java.nio.file.Files; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -58,6 +59,9 @@ import static org.elasticsearch.xpack.security.authc.kerberos.KerberosAuthentica */ public final class KerberosRealm extends Realm implements CachingRealm { + public static final String KRB_METADATA_REALM_NAME_KEY = "kerberos_realm"; + public static final String KRB_METADATA_UPN_KEY = "kerberos_user_principal_name"; + private final Cache userPrincipalNameToUserCache; private final NativeRoleMappingStore userRoleMapper; private final KerberosTicketValidator kerberosTicketValidator; @@ -151,8 +155,7 @@ public final class KerberosRealm extends Realm implements CachingRealm { kerberosTicketValidator.validateTicket((byte[]) kerbAuthnToken.credentials(), keytabPath, enableKerberosDebug, ActionListener.wrap(userPrincipalNameOutToken -> { if (userPrincipalNameOutToken.v1() != null) { - final String username = maybeRemoveRealmName(userPrincipalNameOutToken.v1()); - resolveUser(username, userPrincipalNameOutToken.v2(), listener); + resolveUser(userPrincipalNameOutToken.v1(), userPrincipalNameOutToken.v2(), listener); } else { /** * This is when security context could not be established may be due to ongoing @@ -171,23 +174,8 @@ public final class KerberosRealm extends Realm implements CachingRealm { }, e -> handleException(e, listener))); } - /** - * Usually principal names are in the form 'user/instance@REALM'. This method - * removes '@REALM' part from the principal name if - * {@link KerberosRealmSettings#SETTING_REMOVE_REALM_NAME} is {@code true} else - * will return the input string. - * - * @param principalName user principal name - * @return username after removal of realm - */ - protected String maybeRemoveRealmName(final String principalName) { - if (this.removeRealmName) { - int foundAtIndex = principalName.indexOf('@'); - if (foundAtIndex > 0) { - return principalName.substring(0, foundAtIndex); - } - } - return principalName; + private String[] splitUserPrincipalName(final String userPrincipalName) { + return userPrincipalName.split("@"); } private void handleException(Exception e, final ActionListener listener) { @@ -205,13 +193,21 @@ public final class KerberosRealm extends Realm implements CachingRealm { } } - private void resolveUser(final String username, final String outToken, final ActionListener listener) { + private void resolveUser(final String userPrincipalName, final String outToken, final ActionListener listener) { // if outToken is present then it needs to be communicated with peer, add it to // response header in thread context. if (Strings.hasText(outToken)) { threadPool.getThreadContext().addResponseHeader(WWW_AUTHENTICATE, NEGOTIATE_AUTH_HEADER_PREFIX + outToken); } + final String[] userAndRealmName = splitUserPrincipalName(userPrincipalName); + /* + * Usually principal names are in the form 'user/instance@REALM'. If + * KerberosRealmSettings#SETTING_REMOVE_REALM_NAME is true then remove + * '@REALM' part from the user principal name to get username. + */ + final String username = (this.removeRealmName) ? userAndRealmName[0] : userPrincipalName; + if (delegatedRealms.hasDelegation()) { delegatedRealms.resolve(username, listener); } else { @@ -219,15 +215,19 @@ public final class KerberosRealm extends Realm implements CachingRealm { if (user != null) { listener.onResponse(AuthenticationResult.success(user)); } else { - buildUser(username, listener); + final String realmName = (userAndRealmName.length > 1) ? userAndRealmName[1] : null; + final Map metadata = new HashMap<>(); + metadata.put(KRB_METADATA_REALM_NAME_KEY, realmName); + metadata.put(KRB_METADATA_UPN_KEY, userPrincipalName); + buildUser(username, metadata, listener); } } } - private void buildUser(final String username, final ActionListener listener) { - final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), null, this.config); + private void buildUser(final String username, final Map metadata, final ActionListener listener) { + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(username, null, Collections.emptySet(), metadata, this.config); userRoleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { - final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, null, true); + final User computedUser = new User(username, roles.toArray(new String[roles.size()]), null, null, userData.getMetadata(), true); if (userPrincipalNameToUserCache != null) { userPrincipalNameToUserCache.put(username, computedUser); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java index 7c5904d048a..dcb087ff147 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmAuthenticateFailedTests.java @@ -25,7 +25,9 @@ import org.ietf.jgss.GSSException; import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.security.auth.login.LoginException; @@ -86,7 +88,10 @@ public class KerberosRealmAuthenticateFailedTests extends KerberosRealmTestCase assertThat(result, is(notNullValue())); if (validTicket) { final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); assertSuccessAuthenticationResult(expectedUser, outToken, result); } else { assertThat(result.getStatus(), is(equalTo(AuthenticationResult.Status.TERMINATE))); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java index ee2e2675e18..2bef16883bb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmCacheTests.java @@ -18,7 +18,9 @@ import org.ietf.jgss.GSSException; import java.io.IOException; import java.nio.file.Path; import java.util.Arrays; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.security.auth.login.LoginException; @@ -40,7 +42,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); @@ -72,7 +77,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); mockKerberosTicketValidator(decodedTicket, keytabPath, krbDebug, new Tuple<>(authNUsername, outToken), null); final String expectedUsername = maybeRemoveRealmName(authNUsername); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(authNUsername)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, authNUsername); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final KerberosAuthenticationToken kerberosAuthenticationToken = new KerberosAuthenticationToken(decodedTicket); final User user1 = authenticateAndAssertResult(kerberosRealm, expectedUser, kerberosAuthenticationToken, outToken); @@ -110,7 +118,10 @@ public class KerberosRealmCacheTests extends KerberosRealmTestCase { final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = randomByteArrayOfLength(10); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java index 8f959a26bb8..4c0b77e320a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTestCase.java @@ -160,6 +160,7 @@ public abstract class KerberosRealmTestCase extends ESTestCase { if (withInstance) { principalName.append("/").append(randomAlphaOfLength(5)); } + principalName.append("@"); principalName.append(randomAlphaOfLength(5).toUpperCase(Locale.ROOT)); return principalName.toString(); } @@ -183,6 +184,19 @@ public abstract class KerberosRealmTestCase extends ESTestCase { return principalName; } + /** + * Extracts and returns realm part from the principal name. + * @param principalName user principal name + * @return realm name if found else returns {@code null} + */ + protected String realmName(final String principalName) { + String[] values = principalName.split("@"); + if (values.length > 1) { + return values[1]; + } + return null; + } + /** * Write content to provided keytab file. * diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java index 1166e929341..3c7c3d3473f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/kerberos/KerberosRealmTests.java @@ -38,7 +38,9 @@ import java.nio.file.attribute.PosixFilePermissions; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.Locale; +import java.util.Map; import java.util.Set; import javax.security.auth.login.LoginException; @@ -71,7 +73,10 @@ public class KerberosRealmTests extends KerberosRealmTestCase { final String username = randomPrincipalName(); final KerberosRealm kerberosRealm = createKerberosRealm(username); final String expectedUsername = maybeRemoveRealmName(username); - final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, null, true); + final Map metadata = new HashMap<>(); + metadata.put(KerberosRealm.KRB_METADATA_REALM_NAME_KEY, realmName(username)); + metadata.put(KerberosRealm.KRB_METADATA_UPN_KEY, username); + final User expectedUser = new User(expectedUsername, roles.toArray(new String[roles.size()]), null, null, metadata, true); final byte[] decodedTicket = "base64encodedticket".getBytes(StandardCharsets.UTF_8); final Path keytabPath = config.env().configFile().resolve(KerberosRealmSettings.HTTP_SERVICE_KEYTAB_PATH.get(config.settings())); final boolean krbDebug = KerberosRealmSettings.SETTING_KRB_DEBUG_ENABLE.get(config.settings()); From e9826164bddf0263f0a826e6d2824d5f103bb5b4 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 14 Sep 2018 10:14:02 +0400 Subject: [PATCH 24/45] Mute FullClusterRestartSettingsUpgradeIT Tracked by #33697 --- .../upgrades/FullClusterRestartSettingsUpgradeIT.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java index 19fbdc92fae..5e3ccc75b74 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -19,6 +19,7 @@ package org.elasticsearch.upgrades; +import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.client.Request; @@ -39,6 +40,7 @@ import static org.elasticsearch.transport.RemoteClusterAware.SEARCH_REMOTE_CLUST import static org.elasticsearch.transport.RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE; import static org.hamcrest.Matchers.equalTo; +@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33697") public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { public void testRemoteClusterSettingsUpgraded() throws IOException { From c9131983f53d91d6161a9a1e7747c70b28dc0455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 14 Sep 2018 09:41:20 +0200 Subject: [PATCH 25/45] [Docs] Minor fix in `has_child` javadoc comment (#33674) The min and max constants are accidentaly the wrong way around. --- .../org/elasticsearch/join/query/HasChildQueryBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java index e37a7960091..696c4a72bdb 100644 --- a/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java +++ b/modules/parent-join/src/main/java/org/elasticsearch/join/query/HasChildQueryBuilder.java @@ -183,7 +183,7 @@ public class HasChildQueryBuilder extends AbstractQueryBuilder Date: Fri, 14 Sep 2018 09:59:06 +0200 Subject: [PATCH 26/45] [TEST] wait for no initializing shards --- .../java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java | 1 + .../src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java | 1 + 2 files changed, 2 insertions(+) diff --git a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java index 43b16727aac..851a292ddae 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster-with-security/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexSecurityIT.java @@ -243,6 +243,7 @@ public class FollowIndexSecurityIT extends ESRestTestCase { Request request = new Request("GET", "/_cluster/health/" + index); request.addParameter("wait_for_status", "yellow"); request.addParameter("wait_for_no_relocating_shards", "true"); + request.addParameter("wait_for_no_initializing_shards", "true"); request.addParameter("timeout", "70s"); request.addParameter("level", "shards"); adminClient().performRequest(request); diff --git a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java index 5c1c3915044..c7ecbe184de 100644 --- a/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java +++ b/x-pack/plugin/ccr/qa/multi-cluster/src/test/java/org/elasticsearch/xpack/ccr/FollowIndexIT.java @@ -204,6 +204,7 @@ public class FollowIndexIT extends ESRestTestCase { Request request = new Request("GET", "/_cluster/health/" + index); request.addParameter("wait_for_status", "yellow"); request.addParameter("wait_for_no_relocating_shards", "true"); + request.addParameter("wait_for_no_initializing_shards", "true"); request.addParameter("timeout", "70s"); request.addParameter("level", "shards"); client().performRequest(request); From 5f495c18dfd7cc8896b5a84868a843c0602d50e3 Mon Sep 17 00:00:00 2001 From: Jim Ferenczi Date: Fri, 14 Sep 2018 10:22:11 +0200 Subject: [PATCH 27/45] Adapt skip version for doc_values format deprecation This commit fixes bwc rest tests for the doc_values format deprecation in search. The message of the deprecation changed in 6.4.1 so the bwc test should not check against 6.4.0. --- .../test/search/10_source_filtering.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml index 2725580d9e8..a5f50464794 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml +++ b/rest-api-spec/src/main/resources/rest-api-spec/test/search/10_source_filtering.yml @@ -134,8 +134,8 @@ setup: --- "docvalue_fields": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: @@ -148,8 +148,8 @@ setup: --- "multiple docvalue_fields": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: @@ -162,8 +162,8 @@ setup: --- "docvalue_fields as url param": - skip: - version: " - 6.3.99" - reason: format option was added in 6.4 + version: " - 6.4.0" + reason: format option was added in 6.4 and the deprecation message changed in 6.4.1 features: warnings - do: warnings: From 568ac10ca696588dd2c4ce9ee2b0895643e4cef0 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Fri, 14 Sep 2018 09:29:11 +0100 Subject: [PATCH 28/45] [ML] Allow overrides for some file structure detection decisions (#33630) This change modifies the file structure detection functionality such that some of the decisions can be overridden with user supplied values. The fields that can be overridden are: - charset - format - has_header_row - column_names - delimiter - quote - should_trim_fields - grok_pattern - timestamp_field - timestamp_format If an override makes finding the file structure impossible then the endpoint will return an exception. --- .../ml/action/FindFileStructureAction.java | 217 +++++++++++++- .../ml/filestructurefinder/FileStructure.java | 83 ++++-- .../FindFileStructureActionRequestTests.java | 94 +++++- .../FileStructureTests.java | 1 + .../TransportFindFileStructureAction.java | 5 +- .../DelimitedFileStructureFinder.java | 90 ++++-- .../DelimitedFileStructureFinderFactory.java | 22 +- .../FileStructureFinderFactory.java | 18 +- .../FileStructureFinderManager.java | 85 +++++- .../FileStructureOverrides.java | 205 +++++++++++++ .../FileStructureUtils.java | 59 +++- .../GrokPatternCreator.java | 114 ++++--- .../JsonFileStructureFinder.java | 10 +- .../JsonFileStructureFinderFactory.java | 12 +- .../TextLogFileStructureFinder.java | 41 ++- .../TextLogFileStructureFinderFactory.java | 13 +- .../TimestampFormatFinder.java | 88 ++++-- .../XmlFileStructureFinder.java | 10 +- .../XmlFileStructureFinderFactory.java | 11 +- .../ml/rest/RestFindFileStructureAction.java | 11 + ...imitedFileStructureFinderFactoryTests.java | 8 +- .../DelimitedFileStructureFinderTests.java | 202 ++++++++++++- .../FileStructureFinderManagerTests.java | 62 +++- .../FileStructureUtilsTests.java | 94 ++++-- .../GrokPatternCreatorTests.java | 61 +++- .../JsonFileStructureFinderTests.java | 4 +- .../TextLogFileStructureFinderTests.java | 277 ++++++++++++------ .../XmlFileStructureFinderTests.java | 4 +- .../api/xpack.ml.find_file_structure.json | 43 ++- .../test/ml/find_file_structure.yml | 55 +++- 30 files changed, 1667 insertions(+), 332 deletions(-) create mode 100644 x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java index 9fda416b33b..d10fedfb589 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureAction.java @@ -22,6 +22,9 @@ import org.elasticsearch.rest.RestStatus; import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -109,8 +112,32 @@ public class FindFileStructureAction extends Action columnNames; + private Boolean hasHeaderRow; + private Character delimiter; + private Character quote; + private Boolean shouldTrimFields; + private String grokPattern; + private String timestampFormat; + private String timestampField; private BytesReference sample; public Request() { @@ -124,6 +151,114 @@ public class FindFileStructureAction extends Action getColumnNames() { + return columnNames; + } + + public void setColumnNames(List columnNames) { + this.columnNames = (columnNames == null || columnNames.isEmpty()) ? null : columnNames; + } + + public void setColumnNames(String[] columnNames) { + this.columnNames = (columnNames == null || columnNames.length == 0) ? null : Arrays.asList(columnNames); + } + + public Boolean getHasHeaderRow() { + return hasHeaderRow; + } + + public void setHasHeaderRow(Boolean hasHeaderRow) { + this.hasHeaderRow = hasHeaderRow; + } + + public Character getDelimiter() { + return delimiter; + } + + public void setDelimiter(Character delimiter) { + this.delimiter = delimiter; + } + + public void setDelimiter(String delimiter) { + if (delimiter == null || delimiter.isEmpty()) { + this.delimiter = null; + } else if (delimiter.length() == 1) { + this.delimiter = delimiter.charAt(0); + } else { + throw new IllegalArgumentException(DELIMITER.getPreferredName() + " must be a single character"); + } + } + + public Character getQuote() { + return quote; + } + + public void setQuote(Character quote) { + this.quote = quote; + } + + public void setQuote(String quote) { + if (quote == null || quote.isEmpty()) { + this.quote = null; + } else if (quote.length() == 1) { + this.quote = quote.charAt(0); + } else { + throw new IllegalArgumentException(QUOTE.getPreferredName() + " must be a single character"); + } + } + + public Boolean getShouldTrimFields() { + return shouldTrimFields; + } + + public void setShouldTrimFields(Boolean shouldTrimFields) { + this.shouldTrimFields = shouldTrimFields; + } + + public String getGrokPattern() { + return grokPattern; + } + + public void setGrokPattern(String grokPattern) { + this.grokPattern = (grokPattern == null || grokPattern.isEmpty()) ? null : grokPattern; + } + + public String getTimestampFormat() { + return timestampFormat; + } + + public void setTimestampFormat(String timestampFormat) { + this.timestampFormat = (timestampFormat == null || timestampFormat.isEmpty()) ? null : timestampFormat; + } + + public String getTimestampField() { + return timestampField; + } + + public void setTimestampField(String timestampField) { + this.timestampField = (timestampField == null || timestampField.isEmpty()) ? null : timestampField; + } + public BytesReference getSample() { return sample; } @@ -132,12 +267,41 @@ public class FindFileStructureAction extends Action PARSER = new ObjectParser<>("file_structure", false, Builder::new); @@ -112,12 +113,13 @@ public class FileStructure implements ToXContentObject, Writeable { PARSER.declareString(Builder::setSampleStart, SAMPLE_START); PARSER.declareString(Builder::setCharset, CHARSET); PARSER.declareBoolean(Builder::setHasByteOrderMarker, HAS_BYTE_ORDER_MARKER); - PARSER.declareString((p, c) -> p.setFormat(Format.fromString(c)), STRUCTURE); + PARSER.declareString((p, c) -> p.setFormat(Format.fromString(c)), FORMAT); PARSER.declareString(Builder::setMultilineStartPattern, MULTILINE_START_PATTERN); PARSER.declareString(Builder::setExcludeLinesPattern, EXCLUDE_LINES_PATTERN); PARSER.declareStringArray(Builder::setColumnNames, COLUMN_NAMES); PARSER.declareBoolean(Builder::setHasHeaderRow, HAS_HEADER_ROW); PARSER.declareString((p, c) -> p.setDelimiter(c.charAt(0)), DELIMITER); + PARSER.declareString((p, c) -> p.setQuote(c.charAt(0)), QUOTE); PARSER.declareBoolean(Builder::setShouldTrimFields, SHOULD_TRIM_FIELDS); PARSER.declareString(Builder::setGrokPattern, GROK_PATTERN); PARSER.declareString(Builder::setTimestampField, TIMESTAMP_FIELD); @@ -145,6 +147,7 @@ public class FileStructure implements ToXContentObject, Writeable { private final List columnNames; private final Boolean hasHeaderRow; private final Character delimiter; + private final Character quote; private final Boolean shouldTrimFields; private final String grokPattern; private final List timestampFormats; @@ -156,8 +159,8 @@ public class FileStructure implements ToXContentObject, Writeable { public FileStructure(int numLinesAnalyzed, int numMessagesAnalyzed, String sampleStart, String charset, Boolean hasByteOrderMarker, Format format, String multilineStartPattern, String excludeLinesPattern, List columnNames, - Boolean hasHeaderRow, Character delimiter, Boolean shouldTrimFields, String grokPattern, String timestampField, - List timestampFormats, boolean needClientTimezone, Map mappings, + Boolean hasHeaderRow, Character delimiter, Character quote, Boolean shouldTrimFields, String grokPattern, + String timestampField, List timestampFormats, boolean needClientTimezone, Map mappings, Map fieldStats, List explanation) { this.numLinesAnalyzed = numLinesAnalyzed; @@ -171,6 +174,7 @@ public class FileStructure implements ToXContentObject, Writeable { this.columnNames = (columnNames == null) ? null : Collections.unmodifiableList(new ArrayList<>(columnNames)); this.hasHeaderRow = hasHeaderRow; this.delimiter = delimiter; + this.quote = quote; this.shouldTrimFields = shouldTrimFields; this.grokPattern = grokPattern; this.timestampField = timestampField; @@ -193,6 +197,7 @@ public class FileStructure implements ToXContentObject, Writeable { columnNames = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; hasHeaderRow = in.readOptionalBoolean(); delimiter = in.readBoolean() ? (char) in.readVInt() : null; + quote = in.readBoolean() ? (char) in.readVInt() : null; shouldTrimFields = in.readOptionalBoolean(); grokPattern = in.readOptionalString(); timestampFormats = in.readBoolean() ? Collections.unmodifiableList(in.readList(StreamInput::readString)) : null; @@ -226,6 +231,12 @@ public class FileStructure implements ToXContentObject, Writeable { out.writeBoolean(true); out.writeVInt(delimiter); } + if (quote == null) { + out.writeBoolean(false); + } else { + out.writeBoolean(true); + out.writeVInt(quote); + } out.writeOptionalBoolean(shouldTrimFields); out.writeOptionalString(grokPattern); if (timestampFormats == null) { @@ -285,6 +296,10 @@ public class FileStructure implements ToXContentObject, Writeable { return delimiter; } + public Character getQuote() { + return quote; + } + public Boolean getShouldTrimFields() { return shouldTrimFields; } @@ -328,7 +343,7 @@ public class FileStructure implements ToXContentObject, Writeable { if (hasByteOrderMarker != null) { builder.field(HAS_BYTE_ORDER_MARKER.getPreferredName(), hasByteOrderMarker.booleanValue()); } - builder.field(STRUCTURE.getPreferredName(), format); + builder.field(FORMAT.getPreferredName(), format); if (multilineStartPattern != null && multilineStartPattern.isEmpty() == false) { builder.field(MULTILINE_START_PATTERN.getPreferredName(), multilineStartPattern); } @@ -344,6 +359,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { builder.field(DELIMITER.getPreferredName(), String.valueOf(delimiter)); } + if (quote != null) { + builder.field(QUOTE.getPreferredName(), String.valueOf(quote)); + } if (shouldTrimFields != null) { builder.field(SHOULD_TRIM_FIELDS.getPreferredName(), shouldTrimFields.booleanValue()); } @@ -377,8 +395,8 @@ public class FileStructure implements ToXContentObject, Writeable { public int hashCode() { return Objects.hash(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, timestampField, - timestampFormats, needClientTimezone, mappings, fieldStats, explanation); + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, + timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } @Override @@ -405,6 +423,7 @@ public class FileStructure implements ToXContentObject, Writeable { Objects.equals(this.columnNames, that.columnNames) && Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && Objects.equals(this.delimiter, that.delimiter) && + Objects.equals(this.quote, that.quote) && Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && Objects.equals(this.grokPattern, that.grokPattern) && Objects.equals(this.timestampField, that.timestampField) && @@ -427,6 +446,7 @@ public class FileStructure implements ToXContentObject, Writeable { private List columnNames; private Boolean hasHeaderRow; private Character delimiter; + private Character quote; private Boolean shouldTrimFields; private String grokPattern; private String timestampField; @@ -499,6 +519,11 @@ public class FileStructure implements ToXContentObject, Writeable { return this; } + public Builder setQuote(Character quote) { + this.quote = quote; + return this; + } + public Builder setShouldTrimFields(Boolean shouldTrimFields) { this.shouldTrimFields = shouldTrimFields; return this; @@ -582,6 +607,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } + if (quote != null) { + throw new IllegalArgumentException("Quote may not be specified for [" + format + "] structures."); + } if (grokPattern != null) { throw new IllegalArgumentException("Grok pattern may not be specified for [" + format + "] structures."); } @@ -610,6 +638,9 @@ public class FileStructure implements ToXContentObject, Writeable { if (delimiter != null) { throw new IllegalArgumentException("Delimiter may not be specified for [" + format + "] structures."); } + if (quote != null) { + throw new IllegalArgumentException("Quote may not be specified for [" + format + "] structures."); + } if (shouldTrimFields != null) { throw new IllegalArgumentException("Should trim fields may not be specified for [" + format + "] structures."); } @@ -638,7 +669,7 @@ public class FileStructure implements ToXContentObject, Writeable { } return new FileStructure(numLinesAnalyzed, numMessagesAnalyzed, sampleStart, charset, hasByteOrderMarker, format, - multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, shouldTrimFields, grokPattern, + multilineStartPattern, excludeLinesPattern, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, timestampField, timestampFormats, needClientTimezone, mappings, fieldStats, explanation); } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java index 05ba0e7f306..21f11fa5f73 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/action/FindFileStructureActionRequestTests.java @@ -8,6 +8,9 @@ package org.elasticsearch.xpack.core.ml.action; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + +import java.util.Arrays; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.startsWith; @@ -22,6 +25,44 @@ public class FindFileStructureActionRequestTests extends AbstractStreamableTestC if (randomBoolean()) { request.setLinesToSample(randomIntBetween(10, 2000)); } + + if (randomBoolean()) { + request.setCharset(randomAlphaOfLength(10)); + } + + if (randomBoolean()) { + FileStructure.Format format = randomFrom(FileStructure.Format.values()); + request.setFormat(format); + if (format == FileStructure.Format.DELIMITED) { + if (randomBoolean()) { + request.setColumnNames(generateRandomStringArray(10, 15, false, false)); + } + if (randomBoolean()) { + request.setHasHeaderRow(randomBoolean()); + } + if (randomBoolean()) { + request.setDelimiter(randomFrom(',', '\t', ';', '|')); + } + if (randomBoolean()) { + request.setQuote(randomFrom('"', '\'')); + } + if (randomBoolean()) { + request.setShouldTrimFields(randomBoolean()); + } + } else if (format == FileStructure.Format.SEMI_STRUCTURED_TEXT) { + if (randomBoolean()) { + request.setGrokPattern(randomAlphaOfLength(80)); + } + } + } + + if (randomBoolean()) { + request.setTimestampFormat(randomAlphaOfLength(20)); + } + if (randomBoolean()) { + request.setTimestampField(randomAlphaOfLength(15)); + } + request.setSample(new BytesArray(randomByteArrayOfLength(randomIntBetween(1000, 20000)))); return request; @@ -35,13 +76,62 @@ public class FindFileStructureActionRequestTests extends AbstractStreamableTestC public void testValidateLinesToSample() { FindFileStructureAction.Request request = new FindFileStructureAction.Request(); - request.setLinesToSample(randomFrom(-1, 0)); + request.setLinesToSample(randomIntBetween(-1, 0)); request.setSample(new BytesArray("foo\n")); ActionRequestValidationException e = request.validate(); assertNotNull(e); assertThat(e.getMessage(), startsWith("Validation Failed: ")); - assertThat(e.getMessage(), containsString(" lines_to_sample must be positive if specified")); + assertThat(e.getMessage(), containsString(" [lines_to_sample] must be positive if specified")); + } + + public void testValidateNonDelimited() { + + FindFileStructureAction.Request request = new FindFileStructureAction.Request(); + String errorField; + switch (randomIntBetween(0, 4)) { + case 0: + errorField = "column_names"; + request.setColumnNames(Arrays.asList("col1", "col2")); + break; + case 1: + errorField = "has_header_row"; + request.setHasHeaderRow(randomBoolean()); + break; + case 2: + errorField = "delimiter"; + request.setDelimiter(randomFrom(',', '\t', ';', '|')); + break; + case 3: + errorField = "quote"; + request.setQuote(randomFrom('"', '\'')); + break; + case 4: + errorField = "should_trim_fields"; + request.setShouldTrimFields(randomBoolean()); + break; + default: + throw new IllegalStateException("unexpected switch value"); + } + request.setSample(new BytesArray("foo\n")); + + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.getMessage(), startsWith("Validation Failed: ")); + assertThat(e.getMessage(), containsString(" [" + errorField + "] may only be specified if [format] is [delimited]")); + } + + public void testValidateNonSemiStructuredText() { + + FindFileStructureAction.Request request = new FindFileStructureAction.Request(); + request.setFormat(randomFrom(FileStructure.Format.JSON, FileStructure.Format.XML, FileStructure.Format.DELIMITED)); + request.setGrokPattern(randomAlphaOfLength(80)); + request.setSample(new BytesArray("foo\n")); + + ActionRequestValidationException e = request.validate(); + assertNotNull(e); + assertThat(e.getMessage(), startsWith("Validation Failed: ")); + assertThat(e.getMessage(), containsString(" [grok_pattern] may only be specified if [format] is [semi_structured_text]")); } public void testValidateSample() { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java index e09b9e3f91e..ac6c647136b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ml/filestructurefinder/FileStructureTests.java @@ -54,6 +54,7 @@ public class FileStructureTests extends AbstractSerializingTestCase { @@ -49,8 +50,8 @@ public class TransportFindFileStructureAction FileStructureFinderManager structureFinderManager = new FileStructureFinderManager(); - FileStructureFinder fileStructureFinder = - structureFinderManager.findFileStructure(request.getLinesToSample(), request.getSample().streamInput()); + FileStructureFinder fileStructureFinder = structureFinderManager.findFileStructure(request.getLinesToSample(), + request.getSample().streamInput(), new FileStructureOverrides(request)); return new FindFileStructureAction.Response(fileStructureFinder.getStructure()); } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java index ba6b590dfc8..a103560480d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinder.java @@ -33,6 +33,7 @@ import java.util.stream.IntStream; public class DelimitedFileStructureFinder implements FileStructureFinder { + private static final String REGEX_NEEDS_ESCAPE_PATTERN = "([\\\\|()\\[\\]{}^$.+*?])"; private static final int MAX_LEVENSHTEIN_COMPARISONS = 100; private final List sampleMessages; @@ -40,21 +41,35 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { static DelimitedFileStructureFinder makeDelimitedFileStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, CsvPreference csvPreference, - boolean trimFields) throws IOException { + boolean trimFields, FileStructureOverrides overrides) + throws IOException { Tuple>, List> parsed = readRows(sample, csvPreference); List> rows = parsed.v1(); List lineNumbers = parsed.v2(); - Tuple headerInfo = findHeaderFromSample(explanation, rows); + // Even if the column names are overridden we need to know if there's a + // header in the file, as it affects which rows are considered records + Tuple headerInfo = findHeaderFromSample(explanation, rows, overrides); boolean isHeaderInFile = headerInfo.v1(); String[] header = headerInfo.v2(); - // The column names are the header names but with blanks named column1, column2, etc. - String[] columnNames = new String[header.length]; - for (int i = 0; i < header.length; ++i) { - assert header[i] != null; - String rawHeader = trimFields ? header[i].trim() : header[i]; - columnNames[i] = rawHeader.isEmpty() ? "column" + (i + 1) : rawHeader; + + String[] columnNames; + List overriddenColumnNames = overrides.getColumnNames(); + if (overriddenColumnNames != null) { + if (overriddenColumnNames.size() != header.length) { + throw new IllegalArgumentException("[" + overriddenColumnNames.size() + "] column names were specified [" + + String.join(",", overriddenColumnNames) + "] but there are [" + header.length + "] columns in the sample"); + } + columnNames = overriddenColumnNames.toArray(new String[overriddenColumnNames.size()]); + } else { + // The column names are the header names but with blanks named column1, column2, etc. + columnNames = new String[header.length]; + for (int i = 0; i < header.length; ++i) { + assert header[i] != null; + String rawHeader = trimFields ? header[i].trim() : header[i]; + columnNames[i] = rawHeader.isEmpty() ? "column" + (i + 1) : rawHeader; + } } List sampleLines = Arrays.asList(sample.split("\n")); @@ -84,13 +99,14 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { .setNumMessagesAnalyzed(sampleRecords.size()) .setHasHeaderRow(isHeaderInFile) .setDelimiter(delimiter) + .setQuote(csvPreference.getQuoteChar()) .setColumnNames(Arrays.stream(columnNames).collect(Collectors.toList())); if (trimFields) { structureBuilder.setShouldTrimFields(true); } - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { String timeLineRegex = null; StringBuilder builder = new StringBuilder("^"); @@ -98,7 +114,7 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { // timestamp is the last column then either our assumption is wrong (and the approach will completely // break down) or else every record is on a single line and there's no point creating a multiline config. // This is why the loop excludes the last column. - for (String column : Arrays.asList(header).subList(0, header.length - 1)) { + for (String column : Arrays.asList(columnNames).subList(0, columnNames.length - 1)) { if (timeField.v1().equals(column)) { builder.append("\"?"); String simpleTimePattern = timeField.v2().simplePattern.pattern(); @@ -116,8 +132,11 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { } if (isHeaderInFile) { + String quote = String.valueOf(csvPreference.getQuoteChar()); + String twoQuotes = quote + quote; + String optQuote = quote.replaceAll(REGEX_NEEDS_ESCAPE_PATTERN, "\\\\$1") + "?"; structureBuilder.setExcludeLinesPattern("^" + Arrays.stream(header) - .map(column -> "\"?" + column.replace("\"", "\"\"").replaceAll("([\\\\|()\\[\\]{}^$*?])", "\\\\$1") + "\"?") + .map(column -> optQuote + column.replace(quote, twoQuotes).replaceAll(REGEX_NEEDS_ESCAPE_PATTERN, "\\\\$1") + optQuote) .collect(Collectors.joining(","))); } @@ -131,7 +150,10 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { FileStructureUtils.guessMappingsAndCalculateFieldStats(explanation, sampleRecords); SortedMap mappings = mappingsAndFieldStats.v1(); - mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } if (mappingsAndFieldStats.v2() != null) { structureBuilder.setFieldStats(mappingsAndFieldStats.v2()); @@ -205,45 +227,61 @@ public class DelimitedFileStructureFinder implements FileStructureFinder { return new Tuple<>(rows, lineNumbers); } - static Tuple findHeaderFromSample(List explanation, List> rows) { + static Tuple findHeaderFromSample(List explanation, List> rows, + FileStructureOverrides overrides) { assert rows.isEmpty() == false; + List overriddenColumnNames = overrides.getColumnNames(); List firstRow = rows.get(0); boolean isHeaderInFile = true; - if (rowContainsDuplicateNonEmptyValues(firstRow)) { - isHeaderInFile = false; - explanation.add("First row contains duplicate values, so assuming it's not a header"); + if (overrides.getHasHeaderRow() != null) { + isHeaderInFile = overrides.getHasHeaderRow(); + if (isHeaderInFile && overriddenColumnNames == null) { + String duplicateValue = findDuplicateNonEmptyValues(firstRow); + if (duplicateValue != null) { + throw new IllegalArgumentException("Sample specified to contain a header row, " + + "but the first row contains duplicate values: [" + duplicateValue + "]"); + } + } + explanation.add("Sample specified to " + (isHeaderInFile ? "contain" : "not contain") + " a header row"); } else { - if (rows.size() < 3) { - explanation.add("Too little data to accurately assess whether header is in sample - guessing it is"); + if (findDuplicateNonEmptyValues(firstRow) != null) { + isHeaderInFile = false; + explanation.add("First row contains duplicate values, so assuming it's not a header"); } else { - isHeaderInFile = isFirstRowUnusual(explanation, rows); + if (rows.size() < 3) { + explanation.add("Too little data to accurately assess whether header is in sample - guessing it is"); + } else { + isHeaderInFile = isFirstRowUnusual(explanation, rows); + } } } + String[] header; if (isHeaderInFile) { // SuperCSV will put nulls in the header if any columns don't have names, but empty strings are better for us - return new Tuple<>(true, firstRow.stream().map(field -> (field == null) ? "" : field).toArray(String[]::new)); + header = firstRow.stream().map(field -> (field == null) ? "" : field).toArray(String[]::new); } else { - String[] dummyHeader = new String[firstRow.size()]; - Arrays.fill(dummyHeader, ""); - return new Tuple<>(false, dummyHeader); + header = new String[firstRow.size()]; + Arrays.fill(header, ""); } + + return new Tuple<>(isHeaderInFile, header); } - static boolean rowContainsDuplicateNonEmptyValues(List row) { + static String findDuplicateNonEmptyValues(List row) { HashSet values = new HashSet<>(); for (String value : row) { if (value != null && value.isEmpty() == false && values.add(value) == false) { - return true; + return value; } } - return false; + return null; } private static boolean isFirstRowUnusual(List explanation, List> rows) { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java index 0bbe13e3b05..62e5eff517e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactory.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import org.supercsv.prefs.CsvPreference; import java.io.IOException; @@ -17,12 +18,23 @@ public class DelimitedFileStructureFinderFactory implements FileStructureFinderF private final int minFieldsPerRow; private final boolean trimFields; - DelimitedFileStructureFinderFactory(char delimiter, int minFieldsPerRow, boolean trimFields) { - csvPreference = new CsvPreference.Builder('"', delimiter, "\n").build(); + DelimitedFileStructureFinderFactory(char delimiter, char quote, int minFieldsPerRow, boolean trimFields) { + csvPreference = new CsvPreference.Builder(quote, delimiter, "\n").build(); this.minFieldsPerRow = minFieldsPerRow; this.trimFields = trimFields; } + DelimitedFileStructureFinderFactory makeSimilar(Character quote, Boolean trimFields) { + + return new DelimitedFileStructureFinderFactory((char) csvPreference.getDelimiterChar(), + (quote == null) ? csvPreference.getQuoteChar() : quote, minFieldsPerRow, (trimFields == null) ? this.trimFields : trimFields); + } + + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.DELIMITED; + } + /** * Rules are: * - It must contain at least two complete records @@ -49,9 +61,9 @@ public class DelimitedFileStructureFinderFactory implements FileStructureFinderF } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException { return DelimitedFileStructureFinder.makeDelimitedFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, - csvPreference, trimFields); + csvPreference, trimFields, overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java index 4b6fce322ee..bff4b2115b0 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderFactory.java @@ -5,10 +5,20 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + import java.util.List; public interface FileStructureFinderFactory { + /** + * Can this factory create a {@link FileStructureFinder} that can find the supplied format? + * @param format The format to query, or null. + * @return true if {@code format} is null or the factory + * can produce a {@link FileStructureFinder} that can find {@code format}. + */ + boolean canFindFormat(FileStructure.Format format); + /** * Given a sample of a file, decide whether this factory will be able * to create an appropriate object to represent its ingestion configs. @@ -27,9 +37,11 @@ public interface FileStructureFinderFactory { * @param sample A sample from the file to be ingested. * @param charsetName The name of the character set in which the sample was provided. * @param hasByteOrderMarker Did the sample have a byte order marker? null means "not relevant". - * @return A file structure object suitable for ingesting the supplied sample. + * @param overrides Stores structure decisions that have been made by the end user, and should + * take precedence over anything the {@link FileStructureFinder} may decide. + * @return A {@link FileStructureFinder} object suitable for determining the structure of the supplied sample. * @throws Exception if something goes wrong during creation. */ - FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws Exception; + FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws Exception; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java index d0ce68aff25..7949998d16e 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManager.java @@ -13,6 +13,7 @@ import java.io.BufferedInputStream; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.Reader; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -24,6 +25,7 @@ import java.util.List; import java.util.Locale; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; /** * Runs the high-level steps needed to create ingest configs for the specified file. In order: @@ -70,15 +72,19 @@ public final class FileStructureFinderManager { new JsonFileStructureFinderFactory(), new XmlFileStructureFinderFactory(), // ND-JSON will often also be valid (although utterly weird) CSV, so JSON must come before CSV - new DelimitedFileStructureFinderFactory(',', 2, false), - new DelimitedFileStructureFinderFactory('\t', 2, false), - new DelimitedFileStructureFinderFactory(';', 4, false), - new DelimitedFileStructureFinderFactory('|', 5, true), + new DelimitedFileStructureFinderFactory(',', '"', 2, false), + new DelimitedFileStructureFinderFactory('\t', '"', 2, false), + new DelimitedFileStructureFinderFactory(';', '"', 4, false), + new DelimitedFileStructureFinderFactory('|', '"', 5, true), new TextLogFileStructureFinderFactory() )); private static final int BUFFER_SIZE = 8192; + public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile) throws Exception { + return findFileStructure(idealSampleLineCount, fromFile, FileStructureOverrides.EMPTY_OVERRIDES); + } + /** * Given a stream of data from some file, determine its structure. * @param idealSampleLineCount Ideally, how many lines from the stream will be read to determine the structure? @@ -86,24 +92,42 @@ public final class FileStructureFinderManager { * least {@link #MIN_SAMPLE_LINE_COUNT} lines can be read. If null * the value of {@link #DEFAULT_IDEAL_SAMPLE_LINE_COUNT} will be used. * @param fromFile A stream from which the sample will be read. + * @param overrides Aspects of the file structure that are known in advance. These take precedence over + * values determined by structure analysis. An exception will be thrown if the file structure + * is incompatible with an overridden value. * @return A {@link FileStructureFinder} object from which the structure and messages can be queried. * @throws Exception A variety of problems could occur at various stages of the structure finding process. */ - public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile) throws Exception { + public FileStructureFinder findFileStructure(Integer idealSampleLineCount, InputStream fromFile, FileStructureOverrides overrides) + throws Exception { return findFileStructure(new ArrayList<>(), (idealSampleLineCount == null) ? DEFAULT_IDEAL_SAMPLE_LINE_COUNT : idealSampleLineCount, - fromFile); + fromFile, overrides); } public FileStructureFinder findFileStructure(List explanation, int idealSampleLineCount, InputStream fromFile) throws Exception { + return findFileStructure(new ArrayList<>(), idealSampleLineCount, fromFile, FileStructureOverrides.EMPTY_OVERRIDES); + } - CharsetMatch charsetMatch = findCharset(explanation, fromFile); - String charsetName = charsetMatch.getName(); + public FileStructureFinder findFileStructure(List explanation, int idealSampleLineCount, InputStream fromFile, + FileStructureOverrides overrides) throws Exception { - Tuple sampleInfo = sampleFile(charsetMatch.getReader(), charsetName, MIN_SAMPLE_LINE_COUNT, + String charsetName = overrides.getCharset(); + Reader sampleReader; + if (charsetName != null) { + // Creating the reader will throw if the specified character set does not exist + sampleReader = new InputStreamReader(fromFile, charsetName); + explanation.add("Using specified character encoding [" + charsetName + "]"); + } else { + CharsetMatch charsetMatch = findCharset(explanation, fromFile); + charsetName = charsetMatch.getName(); + sampleReader = charsetMatch.getReader(); + } + + Tuple sampleInfo = sampleFile(sampleReader, charsetName, MIN_SAMPLE_LINE_COUNT, Math.max(MIN_SAMPLE_LINE_COUNT, idealSampleLineCount)); - return makeBestStructureFinder(explanation, sampleInfo.v1(), charsetName, sampleInfo.v2()); + return makeBestStructureFinder(explanation, sampleInfo.v1(), charsetName, sampleInfo.v2(), overrides); } CharsetMatch findCharset(List explanation, InputStream inputStream) throws Exception { @@ -195,15 +219,44 @@ public final class FileStructureFinderManager { (containsZeroBytes ? " - could it be binary data?" : "")); } - FileStructureFinder makeBestStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws Exception { + FileStructureFinder makeBestStructureFinder(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws Exception { - for (FileStructureFinderFactory factory : ORDERED_STRUCTURE_FACTORIES) { + Character delimiter = overrides.getDelimiter(); + Character quote = overrides.getQuote(); + Boolean shouldTrimFields = overrides.getShouldTrimFields(); + List factories; + if (delimiter != null) { + + // If a precise delimiter is specified, we only need one structure finder + // factory, and we'll tolerate as little as one column in the input + factories = Collections.singletonList(new DelimitedFileStructureFinderFactory(delimiter, (quote == null) ? '"' : quote, 1, + (shouldTrimFields == null) ? (delimiter == '|') : shouldTrimFields)); + + } else if (quote != null || shouldTrimFields != null) { + + // The delimiter is not specified, but some other aspect of delimited files is, + // so clone our default delimited factories altering the overridden values + factories = ORDERED_STRUCTURE_FACTORIES.stream().filter(factory -> factory instanceof DelimitedFileStructureFinderFactory) + .map(factory -> ((DelimitedFileStructureFinderFactory) factory).makeSimilar(quote, shouldTrimFields)) + .collect(Collectors.toList()); + + } else { + + // We can use the default factories, but possibly filtered down to a specific format + factories = ORDERED_STRUCTURE_FACTORIES.stream() + .filter(factory -> factory.canFindFormat(overrides.getFormat())).collect(Collectors.toList()); + + } + + for (FileStructureFinderFactory factory : factories) { if (factory.canCreateFromSample(explanation, sample)) { - return factory.createFromSample(explanation, sample, charsetName, hasByteOrderMarker); + return factory.createFromSample(explanation, sample, charsetName, hasByteOrderMarker, overrides); } } - throw new IllegalArgumentException("Input did not match any known formats"); + + throw new IllegalArgumentException("Input did not match " + + ((overrides.getFormat() == null) ? "any known formats" : "the specified format [" + overrides.getFormat() + "]")); } private Tuple sampleFile(Reader reader, String charsetName, int minLines, int maxLines) throws IOException { @@ -233,7 +286,7 @@ public final class FileStructureFinderManager { } if (lineCount < minLines) { - throw new IllegalArgumentException("Input contained too few lines to sample"); + throw new IllegalArgumentException("Input contained too few lines [" + lineCount + "] to obtain a meaningful sample"); } return new Tuple<>(sample.toString(), hasByteOrderMarker); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java new file mode 100644 index 00000000000..e30699c69b7 --- /dev/null +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureOverrides.java @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +package org.elasticsearch.xpack.ml.filestructurefinder; + +import org.elasticsearch.xpack.core.ml.action.FindFileStructureAction; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * An immutable holder for the aspects of file structure detection that can be overridden + * by the end user. Every field can be null, and this means that that + * aspect of the file structure detection is not overridden. + * + * There is no consistency checking in this class. Consistency checking of the different + * fields is done in {@link FindFileStructureAction.Request}. + */ +public class FileStructureOverrides { + + public static final FileStructureOverrides EMPTY_OVERRIDES = new Builder().build(); + + private final String charset; + private final FileStructure.Format format; + private final List columnNames; + private final Boolean hasHeaderRow; + private final Character delimiter; + private final Character quote; + private final Boolean shouldTrimFields; + private final String grokPattern; + private final String timestampFormat; + private final String timestampField; + + public FileStructureOverrides(FindFileStructureAction.Request request) { + + this(request.getCharset(), request.getFormat(), request.getColumnNames(), request.getHasHeaderRow(), request.getDelimiter(), + request.getQuote(), request.getShouldTrimFields(), request.getGrokPattern(), request.getTimestampFormat(), + request.getTimestampField()); + } + + private FileStructureOverrides(String charset, FileStructure.Format format, List columnNames, Boolean hasHeaderRow, + Character delimiter, Character quote, Boolean shouldTrimFields, String grokPattern, + String timestampFormat, String timestampField) { + this.charset = charset; + this.format = format; + this.columnNames = (columnNames == null) ? null : Collections.unmodifiableList(new ArrayList<>(columnNames)); + this.hasHeaderRow = hasHeaderRow; + this.delimiter = delimiter; + this.quote = quote; + this.shouldTrimFields = shouldTrimFields; + this.grokPattern = grokPattern; + this.timestampFormat = timestampFormat; + this.timestampField = timestampField; + } + + public static Builder builder() { + return new Builder(); + } + + public String getCharset() { + return charset; + } + + public FileStructure.Format getFormat() { + return format; + } + + public List getColumnNames() { + return columnNames; + } + + public Boolean getHasHeaderRow() { + return hasHeaderRow; + } + + public Character getDelimiter() { + return delimiter; + } + + public Character getQuote() { + return quote; + } + + public Boolean getShouldTrimFields() { + return shouldTrimFields; + } + + public String getGrokPattern() { + return grokPattern; + } + + public String getTimestampFormat() { + return timestampFormat; + } + + public String getTimestampField() { + return timestampField; + } + + @Override + public int hashCode() { + + return Objects.hash(charset, format, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, timestampFormat, + timestampField); + } + + @Override + public boolean equals(Object other) { + + if (this == other) { + return true; + } + + if (other == null || getClass() != other.getClass()) { + return false; + } + + FileStructureOverrides that = (FileStructureOverrides) other; + return Objects.equals(this.charset, that.charset) && + Objects.equals(this.format, that.format) && + Objects.equals(this.columnNames, that.columnNames) && + Objects.equals(this.hasHeaderRow, that.hasHeaderRow) && + Objects.equals(this.delimiter, that.delimiter) && + Objects.equals(this.quote, that.quote) && + Objects.equals(this.shouldTrimFields, that.shouldTrimFields) && + Objects.equals(this.grokPattern, that.grokPattern) && + Objects.equals(this.timestampFormat, that.timestampFormat) && + Objects.equals(this.timestampField, that.timestampField); + } + + public static class Builder { + + private String charset; + private FileStructure.Format format; + private List columnNames; + private Boolean hasHeaderRow; + private Character delimiter; + private Character quote; + private Boolean shouldTrimFields; + private String grokPattern; + private String timestampFormat; + private String timestampField; + + public Builder setCharset(String charset) { + this.charset = charset; + return this; + } + + public Builder setFormat(FileStructure.Format format) { + this.format = format; + return this; + } + + public Builder setColumnNames(List columnNames) { + this.columnNames = columnNames; + return this; + } + + public Builder setHasHeaderRow(Boolean hasHeaderRow) { + this.hasHeaderRow = hasHeaderRow; + return this; + } + + public Builder setDelimiter(Character delimiter) { + this.delimiter = delimiter; + return this; + } + + public Builder setQuote(Character quote) { + this.quote = quote; + return this; + } + + public Builder setShouldTrimFields(Boolean shouldTrimFields) { + this.shouldTrimFields = shouldTrimFields; + return this; + } + + public Builder setGrokPattern(String grokPattern) { + this.grokPattern = grokPattern; + return this; + } + + public Builder setTimestampFormat(String timestampFormat) { + this.timestampFormat = timestampFormat; + return this; + } + + public Builder setTimestampField(String timestampField) { + this.timestampField = timestampField; + return this; + } + + public FileStructureOverrides build() { + + return new FileStructureOverrides(charset, format, columnNames, hasHeaderRow, delimiter, quote, shouldTrimFields, grokPattern, + timestampFormat, timestampField); + } + } +} diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java index 0341e03a20b..66ecee5b311 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtils.java @@ -51,29 +51,41 @@ public final class FileStructureUtils { * may be non-empty when the method is called, and this method may * append to it. * @param sampleRecords List of records derived from the provided sample. + * @param overrides Aspects of the file structure that are known in advance. These take precedence over + * values determined by structure analysis. An exception will be thrown if the file structure + * is incompatible with an overridden value. * @return A tuple of (field name, timestamp format) if one can be found, or null if * there is no consistent timestamp. */ - static Tuple guessTimestampField(List explanation, List> sampleRecords) { + static Tuple guessTimestampField(List explanation, List> sampleRecords, + FileStructureOverrides overrides) { if (sampleRecords.isEmpty()) { return null; } // Accept the first match from the first sample that is compatible with all the other samples - for (Tuple candidate : findCandidates(explanation, sampleRecords)) { + for (Tuple candidate : findCandidates(explanation, sampleRecords, overrides)) { boolean allGood = true; for (Map sampleRecord : sampleRecords.subList(1, sampleRecords.size())) { Object fieldValue = sampleRecord.get(candidate.v1()); if (fieldValue == null) { + if (overrides.getTimestampField() != null) { + throw new IllegalArgumentException("Specified timestamp field [" + overrides.getTimestampField() + + "] is not present in record [" + sampleRecord + "]"); + } explanation.add("First sample match [" + candidate.v1() + "] ruled out because record [" + sampleRecord + "] doesn't have field"); allGood = false; break; } - TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(fieldValue.toString()); + TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(fieldValue.toString(), overrides.getTimestampFormat()); if (match == null || match.candidateIndex != candidate.v2().candidateIndex) { + if (overrides.getTimestampFormat() != null) { + throw new IllegalArgumentException("Specified timestamp format [" + overrides.getTimestampFormat() + + "] does not match for record [" + sampleRecord + "]"); + } explanation.add("First sample match [" + candidate.v1() + "] ruled out because record [" + sampleRecord + "] matches differently: [" + match + "]"); allGood = false; @@ -82,7 +94,8 @@ public final class FileStructureUtils { } if (allGood) { - explanation.add("Guessing timestamp field is [" + candidate.v1() + "] with format [" + candidate.v2() + "]"); + explanation.add(((overrides.getTimestampField() == null) ? "Guessing timestamp" : "Timestamp") + + " field is [" + candidate.v1() + "] with format [" + candidate.v2() + "]"); return candidate; } } @@ -90,23 +103,41 @@ public final class FileStructureUtils { return null; } - private static List> findCandidates(List explanation, List> sampleRecords) { + private static List> findCandidates(List explanation, List> sampleRecords, + FileStructureOverrides overrides) { + + assert sampleRecords.isEmpty() == false; + Map firstRecord = sampleRecords.get(0); + + String onlyConsiderField = overrides.getTimestampField(); + if (onlyConsiderField != null && firstRecord.get(onlyConsiderField) == null) { + throw new IllegalArgumentException("Specified timestamp field [" + overrides.getTimestampField() + + "] is not present in record [" + firstRecord + "]"); + } List> candidates = new ArrayList<>(); - // Get candidate timestamps from the first sample record - for (Map.Entry entry : sampleRecords.get(0).entrySet()) { - Object value = entry.getValue(); - if (value != null) { - TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(value.toString()); - if (match != null) { - Tuple candidate = new Tuple<>(entry.getKey(), match); - candidates.add(candidate); - explanation.add("First sample timestamp match [" + candidate + "]"); + // Get candidate timestamps from the possible field(s) of the first sample record + for (Map.Entry field : firstRecord.entrySet()) { + String fieldName = field.getKey(); + if (onlyConsiderField == null || onlyConsiderField.equals(fieldName)) { + Object value = field.getValue(); + if (value != null) { + TimestampMatch match = TimestampFormatFinder.findFirstFullMatch(value.toString(), overrides.getTimestampFormat()); + if (match != null) { + Tuple candidate = new Tuple<>(fieldName, match); + candidates.add(candidate); + explanation.add("First sample timestamp match [" + candidate + "]"); + } } } } + if (candidates.isEmpty() && overrides.getTimestampFormat() != null) { + throw new IllegalArgumentException("Specified timestamp format [" + overrides.getTimestampFormat() + + "] does not match for record [" + firstRecord + "]"); + } + return candidates; } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java index 292d0b8e8b3..54be5079c9d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreator.java @@ -48,21 +48,21 @@ public final class GrokPatternCreator { * Grok patterns that are designed to match the whole message, not just a part of it. */ private static final List FULL_MATCH_GROK_PATTERNS = Arrays.asList( - new FullMatchGrokPatternCandidate("BACULA_LOGLINE", "bts"), - new FullMatchGrokPatternCandidate("CATALINALOG", "timestamp"), - new FullMatchGrokPatternCandidate("COMBINEDAPACHELOG", "timestamp"), - new FullMatchGrokPatternCandidate("COMMONAPACHELOG", "timestamp"), - new FullMatchGrokPatternCandidate("ELB_ACCESS_LOG", "timestamp"), - new FullMatchGrokPatternCandidate("HAPROXYHTTP", "syslog_timestamp"), - new FullMatchGrokPatternCandidate("HAPROXYTCP", "syslog_timestamp"), - new FullMatchGrokPatternCandidate("HTTPD20_ERRORLOG", "timestamp"), - new FullMatchGrokPatternCandidate("HTTPD24_ERRORLOG", "timestamp"), - new FullMatchGrokPatternCandidate("NAGIOSLOGLINE", "nagios_epoch"), - new FullMatchGrokPatternCandidate("NETSCREENSESSIONLOG", "date"), - new FullMatchGrokPatternCandidate("RAILS3", "timestamp"), - new FullMatchGrokPatternCandidate("RUBY_LOGGER", "timestamp"), - new FullMatchGrokPatternCandidate("SHOREWALL", "timestamp"), - new FullMatchGrokPatternCandidate("TOMCATLOG", "timestamp") + FullMatchGrokPatternCandidate.fromGrokPatternName("BACULA_LOGLINE", "bts"), + FullMatchGrokPatternCandidate.fromGrokPatternName("CATALINALOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("COMBINEDAPACHELOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("COMMONAPACHELOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("ELB_ACCESS_LOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HAPROXYHTTP", "syslog_timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HAPROXYTCP", "syslog_timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HTTPD20_ERRORLOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("HTTPD24_ERRORLOG", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("NAGIOSLOGLINE", "nagios_epoch"), + FullMatchGrokPatternCandidate.fromGrokPatternName("NETSCREENSESSIONLOG", "date"), + FullMatchGrokPatternCandidate.fromGrokPatternName("RAILS3", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("RUBY_LOGGER", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("SHOREWALL", "timestamp"), + FullMatchGrokPatternCandidate.fromGrokPatternName("TOMCATLOG", "timestamp") ); /** @@ -87,7 +87,7 @@ public final class GrokPatternCreator { // Can't use \b as the breaks, because slashes are not "word" characters new ValueOnlyGrokPatternCandidate("PATH", "keyword", "path", "(?null. + * @param timestampField If not null then the chosen Grok pattern must use this timestamp field. * @return A tuple of (time field name, Grok string), or null if no suitable Grok pattern was found. */ - public Tuple findFullLineGrokPattern() { + public Tuple findFullLineGrokPattern(String timestampField) { for (FullMatchGrokPatternCandidate candidate : FULL_MATCH_GROK_PATTERNS) { - if (candidate.matchesAll(sampleMessages)) { - return candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + if (timestampField == null || timestampField.equals(candidate.getTimeField())) { + if (candidate.matchesAll(sampleMessages)) { + return candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + } } } return null; } + /** + * This method processes a user-supplied Grok pattern that will match all of the sample messages in their entirety. + * It will also update mappings and field stats if they are non-null. + * @param grokPattern The user supplied Grok pattern. + * @param timestampField The name of the timestamp field within the Grok pattern. + * @throws IllegalArgumentException If the supplied Grok pattern does not match the sample messages. + */ + public void validateFullLineGrokPattern(String grokPattern, String timestampField) { + + FullMatchGrokPatternCandidate candidate = FullMatchGrokPatternCandidate.fromGrokPattern(grokPattern, timestampField); + if (candidate.matchesAll(sampleMessages)) { + candidate.processMatch(explanation, sampleMessages, mappings, fieldStats); + } else { + throw new IllegalArgumentException("Supplied Grok pattern [" + grokPattern + "] does not match sample messages"); + } + } + /** * Build a Grok pattern that will match all of the sample messages in their entirety. * @param seedPatternName A pattern that has already been determined to match some portion of every sample message. @@ -564,14 +584,26 @@ public final class GrokPatternCreator { */ static class FullMatchGrokPatternCandidate { - private final String grokString; + private final String grokPattern; private final String timeField; private final Grok grok; - FullMatchGrokPatternCandidate(String grokPatternName, String timeField) { - grokString = "%{" + grokPatternName + "}"; + static FullMatchGrokPatternCandidate fromGrokPatternName(String grokPatternName, String timeField) { + return new FullMatchGrokPatternCandidate("%{" + grokPatternName + "}", timeField); + } + + static FullMatchGrokPatternCandidate fromGrokPattern(String grokPattern, String timeField) { + return new FullMatchGrokPatternCandidate(grokPattern, timeField); + } + + private FullMatchGrokPatternCandidate(String grokPattern, String timeField) { + this.grokPattern = grokPattern; this.timeField = timeField; - grok = new Grok(Grok.getBuiltinPatterns(), grokString); + grok = new Grok(Grok.getBuiltinPatterns(), grokPattern); + } + + public String getTimeField() { + return timeField; } public boolean matchesAll(Collection sampleMessages) { @@ -585,7 +617,7 @@ public final class GrokPatternCreator { public Tuple processMatch(List explanation, Collection sampleMessages, Map mappings, Map fieldStats) { - explanation.add("A full message Grok pattern [" + grokString.substring(2, grokString.length() - 1) + "] looks appropriate"); + explanation.add("A full message Grok pattern [" + grokPattern.substring(2, grokPattern.length() - 1) + "] looks appropriate"); if (mappings != null || fieldStats != null) { Map> valuesPerField = new HashMap<>(); @@ -594,41 +626,39 @@ public final class GrokPatternCreator { Map captures = grok.captures(sampleMessage); // If the pattern doesn't match then captures will be null if (captures == null) { - throw new IllegalStateException("[" + grokString + "] does not match snippet [" + sampleMessage + "]"); + throw new IllegalStateException("[" + grokPattern + "] does not match snippet [" + sampleMessage + "]"); } for (Map.Entry capture : captures.entrySet()) { String fieldName = capture.getKey(); String fieldValue = capture.getValue().toString(); - - // Exclude the time field because that will be dropped and replaced with @timestamp - if (fieldName.equals(timeField) == false) { - valuesPerField.compute(fieldName, (k, v) -> { - if (v == null) { - return new ArrayList<>(Collections.singletonList(fieldValue)); - } else { - v.add(fieldValue); - return v; - } - }); - } + valuesPerField.compute(fieldName, (k, v) -> { + if (v == null) { + return new ArrayList<>(Collections.singletonList(fieldValue)); + } else { + v.add(fieldValue); + return v; + } + }); } } for (Map.Entry> valuesForField : valuesPerField.entrySet()) { String fieldName = valuesForField.getKey(); if (mappings != null) { - mappings.put(fieldName, - FileStructureUtils.guessScalarMapping(explanation, fieldName, valuesForField.getValue())); + // Exclude the time field because that will be dropped and replaced with @timestamp + if (fieldName.equals(timeField) == false) { + mappings.put(fieldName, + FileStructureUtils.guessScalarMapping(explanation, fieldName, valuesForField.getValue())); + } } if (fieldStats != null) { - fieldStats.put(fieldName, - FileStructureUtils.calculateFieldStats(valuesForField.getValue())); + fieldStats.put(fieldName, FileStructureUtils.calculateFieldStats(valuesForField.getValue())); } } } - return new Tuple<>(timeField, grokString); + return new Tuple<>(timeField, grokPattern); } } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java index a488549bc52..b20658f872b 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinder.java @@ -33,7 +33,8 @@ public class JsonFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static JsonFileStructureFinder makeJsonFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) throws IOException { + Boolean hasByteOrderMarker, FileStructureOverrides overrides) + throws IOException { List> sampleRecords = new ArrayList<>(); @@ -51,7 +52,7 @@ public class JsonFileStructureFinder implements FileStructureFinder { .setNumLinesAnalyzed(sampleMessages.size()) .setNumMessagesAnalyzed(sampleRecords.size()); - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { structureBuilder.setTimestampField(timeField.v1()) .setTimestampFormats(timeField.v2().dateFormats) @@ -62,7 +63,10 @@ public class JsonFileStructureFinder implements FileStructureFinder { FileStructureUtils.guessMappingsAndCalculateFieldStats(explanation, sampleRecords); SortedMap mappings = mappingsAndFieldStats.v1(); - mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + mappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } if (mappingsAndFieldStats.v2() != null) { structureBuilder.setFieldStats(mappingsAndFieldStats.v2()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java index 02be3c1cf19..cfeaa222679 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderFactory.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.ml.filestructurefinder; import org.elasticsearch.common.xcontent.DeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.IOException; import java.io.StringReader; @@ -18,6 +19,11 @@ import static org.elasticsearch.common.xcontent.json.JsonXContent.jsonXContent; public class JsonFileStructureFinderFactory implements FileStructureFinderFactory { + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.JSON; + } + /** * This format matches if the sample consists of one or more JSON documents. * If there is more than one, they must be newline-delimited. The @@ -61,9 +67,9 @@ public class JsonFileStructureFinderFactory implements FileStructureFinderFactor } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) - throws IOException { - return JsonFileStructureFinder.makeJsonFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException { + return JsonFileStructureFinder.makeJsonFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, overrides); } private static class ContextPrintingStringReader extends StringReader { diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java index 95e0a5dc69d..e6e445a3ff6 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinder.java @@ -28,17 +28,19 @@ public class TextLogFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static TextLogFileStructureFinder makeTextLogFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) { + Boolean hasByteOrderMarker, FileStructureOverrides overrides) { String[] sampleLines = sample.split("\n"); - Tuple> bestTimestamp = mostLikelyTimestamp(sampleLines); + Tuple> bestTimestamp = mostLikelyTimestamp(sampleLines, overrides); if (bestTimestamp == null) { // Is it appropriate to treat a file that is neither structured nor has // a regular pattern of timestamps as a log file? Probably not... - throw new IllegalArgumentException("Could not find a timestamp in the sample provided"); + throw new IllegalArgumentException("Could not find " + + ((overrides.getTimestampFormat() == null) ? "a timestamp" : "the specified timestamp format") + " in the sample provided"); } - explanation.add("Most likely timestamp format is [" + bestTimestamp.v1() + "]"); + explanation.add(((overrides.getTimestampFormat() == null) ? "Most likely timestamp" : "Timestamp") + " format is [" + + bestTimestamp.v1() + "]"); List sampleMessages = new ArrayList<>(); StringBuilder preamble = new StringBuilder(); @@ -86,17 +88,26 @@ public class TextLogFileStructureFinder implements FileStructureFinder { SortedMap fieldStats = new TreeMap<>(); - // We can't parse directly into @timestamp using Grok, so parse to some other time field, which the date filter will then remove - String interimTimestampField; - String grokPattern; GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, fieldStats); - Tuple timestampFieldAndFullMatchGrokPattern = grokPatternCreator.findFullLineGrokPattern(); - if (timestampFieldAndFullMatchGrokPattern != null) { - interimTimestampField = timestampFieldAndFullMatchGrokPattern.v1(); - grokPattern = timestampFieldAndFullMatchGrokPattern.v2(); + // We can't parse directly into @timestamp using Grok, so parse to some other time field, which the date filter will then remove + String interimTimestampField = overrides.getTimestampField(); + String grokPattern = overrides.getGrokPattern(); + if (grokPattern != null) { + if (interimTimestampField == null) { + interimTimestampField = "timestamp"; + } + grokPatternCreator.validateFullLineGrokPattern(grokPattern, interimTimestampField); } else { - interimTimestampField = "timestamp"; - grokPattern = grokPatternCreator.createGrokPatternFromExamples(bestTimestamp.v1().grokPatternName, interimTimestampField); + Tuple timestampFieldAndFullMatchGrokPattern = grokPatternCreator.findFullLineGrokPattern(interimTimestampField); + if (timestampFieldAndFullMatchGrokPattern != null) { + interimTimestampField = timestampFieldAndFullMatchGrokPattern.v1(); + grokPattern = timestampFieldAndFullMatchGrokPattern.v2(); + } else { + if (interimTimestampField == null) { + interimTimestampField = "timestamp"; + } + grokPattern = grokPatternCreator.createGrokPatternFromExamples(bestTimestamp.v1().grokPatternName, interimTimestampField); + } } FileStructure structure = structureBuilder @@ -127,14 +138,14 @@ public class TextLogFileStructureFinder implements FileStructureFinder { return structure; } - static Tuple> mostLikelyTimestamp(String[] sampleLines) { + static Tuple> mostLikelyTimestamp(String[] sampleLines, FileStructureOverrides overrides) { Map>> timestampMatches = new LinkedHashMap<>(); int remainingLines = sampleLines.length; double differenceBetweenTwoHighestWeights = 0.0; for (String sampleLine : sampleLines) { - TimestampMatch match = TimestampFormatFinder.findFirstMatch(sampleLine); + TimestampMatch match = TimestampFormatFinder.findFirstMatch(sampleLine, overrides.getTimestampFormat()); if (match != null) { TimestampMatch pureMatch = new TimestampMatch(match.candidateIndex, "", match.dateFormats, match.simplePattern, match.grokPatternName, ""); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java index 5f737eeb9b8..b92b705aaff 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderFactory.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; + import java.util.List; import java.util.regex.Pattern; @@ -13,6 +15,11 @@ public class TextLogFileStructureFinderFactory implements FileStructureFinderFac // This works because, by default, dot doesn't match newlines private static final Pattern TWO_NON_BLANK_LINES_PATTERN = Pattern.compile(".\n+."); + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.SEMI_STRUCTURED_TEXT; + } + /** * This format matches if the sample contains at least one newline and at least two * non-blank lines. @@ -33,7 +40,9 @@ public class TextLogFileStructureFinderFactory implements FileStructureFinderFac } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) { - return TextLogFileStructureFinder.makeTextLogFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) { + return TextLogFileStructureFinder.makeTextLogFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, + overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java index 81e490878a0..363b1352a54 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/TimestampFormatFinder.java @@ -148,6 +148,16 @@ public final class TimestampFormatFinder { return findFirstMatch(text, 0); } + /** + * Find the first timestamp format that matches part of the supplied value. + * @param text The value that the returned timestamp format must exist within. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstMatch(String text, String requiredFormat) { + return findFirstMatch(text, 0, requiredFormat); + } + /** * Find the first timestamp format that matches part of the supplied value, * excluding a specified number of candidate formats. @@ -156,26 +166,40 @@ public final class TimestampFormatFinder { * @return The timestamp format, or null if none matches. */ public static TimestampMatch findFirstMatch(String text, int ignoreCandidates) { + return findFirstMatch(text, ignoreCandidates, null); + } + + /** + * Find the first timestamp format that matches part of the supplied value, + * excluding a specified number of candidate formats. + * @param text The value that the returned timestamp format must exist within. + * @param ignoreCandidates The number of candidate formats to exclude from the search. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstMatch(String text, int ignoreCandidates, String requiredFormat) { Boolean[] quickRuleoutMatches = new Boolean[QUICK_RULE_OUT_PATTERNS.size()]; int index = ignoreCandidates; for (CandidateTimestampFormat candidate : ORDERED_CANDIDATE_FORMATS.subList(ignoreCandidates, ORDERED_CANDIDATE_FORMATS.size())) { - boolean quicklyRuledOut = false; - for (Integer quickRuleOutIndex : candidate.quickRuleOutIndices) { - if (quickRuleoutMatches[quickRuleOutIndex] == null) { - quickRuleoutMatches[quickRuleOutIndex] = QUICK_RULE_OUT_PATTERNS.get(quickRuleOutIndex).matcher(text).find(); + if (requiredFormat == null || candidate.dateFormats.contains(requiredFormat)) { + boolean quicklyRuledOut = false; + for (Integer quickRuleOutIndex : candidate.quickRuleOutIndices) { + if (quickRuleoutMatches[quickRuleOutIndex] == null) { + quickRuleoutMatches[quickRuleOutIndex] = QUICK_RULE_OUT_PATTERNS.get(quickRuleOutIndex).matcher(text).find(); + } + if (quickRuleoutMatches[quickRuleOutIndex] == false) { + quicklyRuledOut = true; + break; + } } - if (quickRuleoutMatches[quickRuleOutIndex] == false) { - quicklyRuledOut = true; - break; - } - } - if (quicklyRuledOut == false) { - Map captures = candidate.strictSearchGrok.captures(text); - if (captures != null) { - String preface = captures.getOrDefault(PREFACE, "").toString(); - String epilogue = captures.getOrDefault(EPILOGUE, "").toString(); - return makeTimestampMatch(candidate, index, preface, text.substring(preface.length(), - text.length() - epilogue.length()), epilogue); + if (quicklyRuledOut == false) { + Map captures = candidate.strictSearchGrok.captures(text); + if (captures != null) { + String preface = captures.getOrDefault(PREFACE, "").toString(); + String epilogue = captures.getOrDefault(EPILOGUE, "").toString(); + return makeTimestampMatch(candidate, index, preface, text.substring(preface.length(), + text.length() - epilogue.length()), epilogue); + } } } ++index; @@ -192,6 +216,16 @@ public final class TimestampFormatFinder { return findFirstFullMatch(text, 0); } + /** + * Find the best timestamp format for matching an entire field value. + * @param text The value that the returned timestamp format must match in its entirety. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstFullMatch(String text, String requiredFormat) { + return findFirstFullMatch(text, 0, requiredFormat); + } + /** * Find the best timestamp format for matching an entire field value, * excluding a specified number of candidate formats. @@ -200,11 +234,25 @@ public final class TimestampFormatFinder { * @return The timestamp format, or null if none matches. */ public static TimestampMatch findFirstFullMatch(String text, int ignoreCandidates) { + return findFirstFullMatch(text, ignoreCandidates, null); + } + + /** + * Find the best timestamp format for matching an entire field value, + * excluding a specified number of candidate formats. + * @param text The value that the returned timestamp format must match in its entirety. + * @param ignoreCandidates The number of candidate formats to exclude from the search. + * @param requiredFormat A date format that any returned match must support. + * @return The timestamp format, or null if none matches. + */ + public static TimestampMatch findFirstFullMatch(String text, int ignoreCandidates, String requiredFormat) { int index = ignoreCandidates; for (CandidateTimestampFormat candidate : ORDERED_CANDIDATE_FORMATS.subList(ignoreCandidates, ORDERED_CANDIDATE_FORMATS.size())) { - Map captures = candidate.strictFullMatchGrok.captures(text); - if (captures != null) { - return makeTimestampMatch(candidate, index, "", text, ""); + if (requiredFormat == null || candidate.dateFormats.contains(requiredFormat)) { + Map captures = candidate.strictFullMatchGrok.captures(text); + if (captures != null) { + return makeTimestampMatch(candidate, index, "", text, ""); + } } ++index; } @@ -417,7 +465,7 @@ public final class TimestampFormatFinder { // The (?m) here has the Ruby meaning, which is equivalent to (?s) in Java this.strictSearchGrok = new Grok(Grok.getBuiltinPatterns(), "(?m)%{DATA:" + PREFACE + "}" + strictGrokPattern + "%{GREEDYDATA:" + EPILOGUE + "}"); - this.strictFullMatchGrok = new Grok(Grok.getBuiltinPatterns(), strictGrokPattern); + this.strictFullMatchGrok = new Grok(Grok.getBuiltinPatterns(), "^" + strictGrokPattern + "$"); this.standardGrokPatternName = standardGrokPatternName; assert quickRuleOutIndices.stream() .noneMatch(quickRuleOutIndex -> quickRuleOutIndex < 0 || quickRuleOutIndex >= QUICK_RULE_OUT_PATTERNS.size()); diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java index 570f36f59c0..d5e3fba34c9 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinder.java @@ -38,7 +38,7 @@ public class XmlFileStructureFinder implements FileStructureFinder { private final FileStructure structure; static XmlFileStructureFinder makeXmlFileStructureFinder(List explanation, String sample, String charsetName, - Boolean hasByteOrderMarker) + Boolean hasByteOrderMarker, FileStructureOverrides overrides) throws IOException, ParserConfigurationException, SAXException { String messagePrefix; @@ -90,7 +90,7 @@ public class XmlFileStructureFinder implements FileStructureFinder { .setNumMessagesAnalyzed(sampleRecords.size()) .setMultilineStartPattern("^\\s*<" + topLevelTag); - Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords); + Tuple timeField = FileStructureUtils.guessTimestampField(explanation, sampleRecords, overrides); if (timeField != null) { structureBuilder.setTimestampField(timeField.v1()) .setTimestampFormats(timeField.v2().dateFormats) @@ -110,8 +110,10 @@ public class XmlFileStructureFinder implements FileStructureFinder { secondLevelProperties.put(FileStructureUtils.MAPPING_PROPERTIES_SETTING, innerMappings); SortedMap outerMappings = new TreeMap<>(); outerMappings.put(topLevelTag, secondLevelProperties); - outerMappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, - Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + if (timeField != null) { + outerMappings.put(FileStructureUtils.DEFAULT_TIMESTAMP_FIELD, + Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date")); + } FileStructure structure = structureBuilder .setMappings(outerMappings) diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java index f8536d14375..3079f53931d 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderFactory.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.ml.filestructurefinder; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import org.xml.sax.SAXException; import javax.xml.parsers.ParserConfigurationException; @@ -27,6 +28,11 @@ public class XmlFileStructureFinderFactory implements FileStructureFinderFactory xmlFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); } + @Override + public boolean canFindFormat(FileStructure.Format format) { + return format == null || format == FileStructure.Format.XML; + } + /** * This format matches if the sample consists of one or more XML documents, * all with the same root element name. If there is more than one document, @@ -115,8 +121,9 @@ public class XmlFileStructureFinderFactory implements FileStructureFinderFactory } @Override - public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker) + public FileStructureFinder createFromSample(List explanation, String sample, String charsetName, Boolean hasByteOrderMarker, + FileStructureOverrides overrides) throws IOException, ParserConfigurationException, SAXException { - return XmlFileStructureFinder.makeXmlFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker); + return XmlFileStructureFinder.makeXmlFileStructureFinder(explanation, sample, charsetName, hasByteOrderMarker, overrides); } } diff --git a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java index 83293c7d60e..316a4b56e4a 100644 --- a/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java +++ b/x-pack/plugin/ml/src/main/java/org/elasticsearch/xpack/ml/rest/RestFindFileStructureAction.java @@ -39,6 +39,17 @@ public class RestFindFileStructureAction extends BaseRestHandler { FindFileStructureAction.Request request = new FindFileStructureAction.Request(); request.setLinesToSample(restRequest.paramAsInt(FindFileStructureAction.Request.LINES_TO_SAMPLE.getPreferredName(), FileStructureFinderManager.DEFAULT_IDEAL_SAMPLE_LINE_COUNT)); + request.setCharset(restRequest.param(FindFileStructureAction.Request.CHARSET.getPreferredName())); + request.setFormat(restRequest.param(FindFileStructureAction.Request.FORMAT.getPreferredName())); + request.setColumnNames(restRequest.paramAsStringArray(FindFileStructureAction.Request.COLUMN_NAMES.getPreferredName(), null)); + request.setHasHeaderRow(restRequest.paramAsBoolean(FindFileStructureAction.Request.HAS_HEADER_ROW.getPreferredName(), null)); + request.setDelimiter(restRequest.param(FindFileStructureAction.Request.DELIMITER.getPreferredName())); + request.setQuote(restRequest.param(FindFileStructureAction.Request.QUOTE.getPreferredName())); + request.setShouldTrimFields(restRequest.paramAsBoolean(FindFileStructureAction.Request.SHOULD_TRIM_FIELDS.getPreferredName(), + null)); + request.setGrokPattern(restRequest.param(FindFileStructureAction.Request.GROK_PATTERN.getPreferredName())); + request.setTimestampFormat(restRequest.param(FindFileStructureAction.Request.TIMESTAMP_FORMAT.getPreferredName())); + request.setTimestampField(restRequest.param(FindFileStructureAction.Request.TIMESTAMP_FIELD.getPreferredName())); if (restRequest.hasContent()) { request.setSample(restRequest.content()); } else { diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java index 6bcb827be94..53f3a2a4d4c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderFactoryTests.java @@ -7,10 +7,10 @@ package org.elasticsearch.xpack.ml.filestructurefinder; public class DelimitedFileStructureFinderFactoryTests extends FileStructureTestCase { - private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', 2, false); - private FileStructureFinderFactory tsvFactory = new DelimitedFileStructureFinderFactory('\t', 2, false); - private FileStructureFinderFactory semiColonDelimitedfactory = new DelimitedFileStructureFinderFactory(';', 4, false); - private FileStructureFinderFactory pipeDelimitedFactory = new DelimitedFileStructureFinderFactory('|', 5, true); + private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', '"', 2, false); + private FileStructureFinderFactory tsvFactory = new DelimitedFileStructureFinderFactory('\t', '"', 2, false); + private FileStructureFinderFactory semiColonDelimitedfactory = new DelimitedFileStructureFinderFactory(';', '"', 4, false); + private FileStructureFinderFactory pipeDelimitedFactory = new DelimitedFileStructureFinderFactory('|', '"', 5, true); // CSV - no need to check JSON or XML because they come earlier in the order we check formats diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java index 4e692d58391..decc61a5397 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/DelimitedFileStructureFinderTests.java @@ -19,7 +19,7 @@ import static org.hamcrest.Matchers.arrayContaining; public class DelimitedFileStructureFinderTests extends FileStructureTestCase { - private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', 2, false); + private FileStructureFinderFactory csvFactory = new DelimitedFileStructureFinderFactory(',', '"', 2, false); public void testCreateConfigsGivenCompleteCsv() throws Exception { String sample = "time,message\n" + @@ -29,7 +29,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -43,6 +44,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals("^\"?time\"?,\"?message\"?", structure.getExcludeLinesPattern()); assertEquals("^\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("time", "message"), structure.getColumnNames()); @@ -51,6 +53,76 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCompleteCsvAndColumnNamesOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setColumnNames(Arrays.asList("my_time", "my_message")).build(); + + String sample = "time,message\n" + + "2018-05-17T13:41:23,hello\n" + + "2018-05-17T13:41:32,hello again\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?time\"?,\"?message\"?", structure.getExcludeLinesPattern()); + assertEquals("^\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("my_time", "my_message"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("my_time", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenCompleteCsvAndHasHeaderRowOverride() throws Exception { + + // It's obvious the first row really should be a header row, so by overriding + // detection with the wrong choice the results will be completely changed + FileStructureOverrides overrides = FileStructureOverrides.builder().setHasHeaderRow(false).build(); + + String sample = "time,message\n" + + "2018-05-17T13:41:23,hello\n" + + "2018-05-17T13:41:32,hello again\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertNull(structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertFalse(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("column1", "column2"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertNull(structure.getTimestampField()); + assertNull(structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithIncompleteLastRecord() throws Exception { String sample = "message,time,count\n" + "\"hello\n" + @@ -60,7 +132,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -74,6 +147,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals("^\"?message\"?,\"?time\"?,\"?count\"?", structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("message", "time", "count"), structure.getColumnNames()); @@ -93,7 +167,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -110,6 +185,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -120,6 +196,50 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCsvWithTrailingNullsAndOverriddenTimeField() throws Exception { + + // Default timestamp field is the first field from the start of each row that contains a + // consistent timestamp format, so if we want the second we need an override + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("tpep_dropoff_datetime").build(); + + String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + + "improvement_surcharge,total_amount,,\n" + + "2,2016-12-31 15:15:01,2016-12-31 15:15:09,1,.00,1,N,264,264,2,1,0,0.5,0,0,0.3,1.8,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:10:22,1,1.60,1,N,163,143,2,9,0.5,0.5,0,0,0.3,10.3,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:11:01,1,1.40,1,N,164,229,1,9,0.5,0.5,2.05,0,0.3,12.35,,\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?VendorID\"?,\"?tpep_pickup_datetime\"?,\"?tpep_dropoff_datetime\"?,\"?passenger_count\"?,\"?trip_distance\"?," + + "\"?RatecodeID\"?,\"?store_and_fwd_flag\"?,\"?PULocationID\"?,\"?DOLocationID\"?,\"?payment_type\"?,\"?fare_amount\"?," + + "\"?extra\"?,\"?mta_tax\"?,\"?tip_amount\"?,\"?tolls_amount\"?,\"?improvement_surcharge\"?,\"?total_amount\"?,\"?\"?,\"?\"?", + structure.getExcludeLinesPattern()); + assertEquals("^.*?,.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", + "RatecodeID", "store_and_fwd_flag", "PULocationID", "DOLocationID", "payment_type", "fare_amount", "extra", "mta_tax", + "tip_amount", "tolls_amount", "improvement_surcharge", "total_amount", "column18", "column19"), structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("tpep_dropoff_datetime", structure.getTimestampField()); + assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeader() throws Exception { String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + @@ -131,7 +251,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -148,6 +269,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("VendorID", "tpep_pickup_datetime", "tpep_dropoff_datetime", "passenger_count", "trip_distance", @@ -158,6 +280,53 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenCsvWithTrailingNullsExceptHeaderAndColumnNamesOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setColumnNames(Arrays.asList("my_VendorID", "my_tpep_pickup_datetime", "my_tpep_dropoff_datetime", "my_passenger_count", + "my_trip_distance", "my_RatecodeID", "my_store_and_fwd_flag", "my_PULocationID", "my_DOLocationID", "my_payment_type", + "my_fare_amount", "my_extra", "my_mta_tax", "my_tip_amount", "my_tolls_amount", "my_improvement_surcharge", + "my_total_amount")).build(); + + String sample = "VendorID,tpep_pickup_datetime,tpep_dropoff_datetime,passenger_count,trip_distance,RatecodeID," + + "store_and_fwd_flag,PULocationID,DOLocationID,payment_type,fare_amount,extra,mta_tax,tip_amount,tolls_amount," + + "improvement_surcharge,total_amount\n" + + "2,2016-12-31 15:15:01,2016-12-31 15:15:09,1,.00,1,N,264,264,2,1,0,0.5,0,0,0.3,1.8,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:10:22,1,1.60,1,N,163,143,2,9,0.5,0.5,0,0,0.3,10.3,,\n" + + "1,2016-12-01 00:00:01,2016-12-01 00:11:01,1,1.40,1,N,164,229,1,9,0.5,0.5,2.05,0,0.3,12.35,,\n"; + assertTrue(csvFactory.canCreateFromSample(explanation, sample)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.DELIMITED, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertEquals("^\"?VendorID\"?,\"?tpep_pickup_datetime\"?,\"?tpep_dropoff_datetime\"?,\"?passenger_count\"?,\"?trip_distance\"?," + + "\"?RatecodeID\"?,\"?store_and_fwd_flag\"?,\"?PULocationID\"?,\"?DOLocationID\"?,\"?payment_type\"?,\"?fare_amount\"?," + + "\"?extra\"?,\"?mta_tax\"?,\"?tip_amount\"?,\"?tolls_amount\"?,\"?improvement_surcharge\"?,\"?total_amount\"?", + structure.getExcludeLinesPattern()); + assertEquals("^.*?,\"?\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); + assertTrue(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals(Arrays.asList("my_VendorID", "my_tpep_pickup_datetime", "my_tpep_dropoff_datetime", "my_passenger_count", + "my_trip_distance", "my_RatecodeID", "my_store_and_fwd_flag", "my_PULocationID", "my_DOLocationID", "my_payment_type", + "my_fare_amount", "my_extra", "my_mta_tax", "my_tip_amount", "my_tolls_amount", "my_improvement_surcharge", "my_total_amount"), + structure.getColumnNames()); + assertNull(structure.getGrokPattern()); + assertEquals("my_tpep_pickup_datetime", structure.getTimestampField()); + assertEquals(Collections.singletonList("YYYY-MM-dd HH:mm:ss"), structure.getTimestampFormats()); + } + public void testCreateConfigsGivenCsvWithTimeLastColumn() throws Exception { String sample = "\"pos_id\",\"trip_id\",\"latitude\",\"longitude\",\"altitude\",\"timestamp\"\n" + "\"1\",\"3\",\"4703.7815\",\"1527.4713\",\"359.9\",\"2017-01-19 16:19:04.742113\"\n" + @@ -166,7 +335,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = csvFactory.createFromSample(explanation, sample, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -181,6 +351,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); assertEquals(Character.valueOf(','), structure.getDelimiter()); + assertEquals(Character.valueOf('"'), structure.getQuote()); assertTrue(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals(Arrays.asList("pos_id", "trip_id", "latitude", "longitude", "altitude", "timestamp"), structure.getColumnNames()); @@ -197,7 +368,7 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; Tuple header = DelimitedFileStructureFinder.findHeaderFromSample(explanation, - DelimitedFileStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + DelimitedFileStructureFinder.readRows(withHeader, CsvPreference.EXCEL_PREFERENCE).v1(), FileStructureOverrides.EMPTY_OVERRIDES); assertTrue(header.v1()); assertThat(header.v2(), arrayContaining("time", "airline", "responsetime", "sourcetype")); @@ -210,7 +381,8 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { "2014-06-23 00:00:01Z,KLM,1355.4812,farequote\n"; Tuple header = DelimitedFileStructureFinder.findHeaderFromSample(explanation, - DelimitedFileStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1()); + DelimitedFileStructureFinder.readRows(withoutHeader, CsvPreference.EXCEL_PREFERENCE).v1(), + FileStructureOverrides.EMPTY_OVERRIDES); assertFalse(header.v1()); assertThat(header.v2(), arrayContaining("", "", "", "")); @@ -283,12 +455,12 @@ public class DelimitedFileStructureFinderTests extends FileStructureTestCase { public void testRowContainsDuplicateNonEmptyValues() { - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList("a"))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Collections.singletonList(""))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); - assertTrue(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); - assertTrue(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); - assertFalse(DelimitedFileStructureFinder.rowContainsDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Collections.singletonList("a"))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Collections.singletonList(""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "c"))); + assertEquals("a", DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "a"))); + assertEquals("b", DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "b", "b"))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("a", "", ""))); + assertNull(DelimitedFileStructureFinder.findDuplicateNonEmptyValues(Arrays.asList("", "a", ""))); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java index 10e780f1d34..00929ff474c 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureFinderManagerTests.java @@ -6,12 +6,14 @@ package org.elasticsearch.xpack.ml.filestructurefinder; import com.ibm.icu.text.CharsetMatch; +import org.elasticsearch.xpack.core.ml.filestructurefinder.FileStructure; import java.io.ByteArrayInputStream; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; +import static org.elasticsearch.xpack.ml.filestructurefinder.FileStructureOverrides.EMPTY_OVERRIDES; import static org.hamcrest.Matchers.startsWith; import static org.hamcrest.core.IsInstanceOf.instanceOf; @@ -47,26 +49,62 @@ public class FileStructureFinderManagerTests extends FileStructureTestCase { } public void testMakeBestStructureGivenJson() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, - "{ \"time\": \"2018-05-17T13:41:23\", \"message\": \"hello\" }", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(JsonFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, JSON_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(JsonFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenJsonAndDelimitedOverride() throws Exception { + + // Need to change the quote character from the default of double quotes + // otherwise the quotes in the JSON will stop it parsing as CSV + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setFormat(FileStructure.Format.DELIMITED).setQuote('\'').build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, JSON_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(DelimitedFileStructureFinder.class)); } public void testMakeBestStructureGivenXml() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, - "hello", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(XmlFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, XML_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(XmlFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenXmlAndTextOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setFormat(FileStructure.Format.SEMI_STRUCTURED_TEXT).build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, XML_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(TextLogFileStructureFinder.class)); } public void testMakeBestStructureGivenCsv() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, "time,message\n" + - "2018-05-17T13:41:23,hello\n", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(DelimitedFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, CSV_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(DelimitedFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenCsvAndJsonOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setFormat(FileStructure.Format.JSON).build(); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> structureFinderManager.makeBestStructureFinder(explanation, CSV_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides)); + + assertEquals("Input did not match the specified format [json]", e.getMessage()); } public void testMakeBestStructureGivenText() throws Exception { - assertThat(structureFinderManager.makeBestStructureFinder(explanation, "[2018-05-17T13:41:23] hello\n" + - "[2018-05-17T13:41:24] hello again\n", StandardCharsets.UTF_8.name(), randomBoolean()), - instanceOf(TextLogFileStructureFinder.class)); + assertThat(structureFinderManager.makeBestStructureFinder(explanation, TEXT_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + EMPTY_OVERRIDES), instanceOf(TextLogFileStructureFinder.class)); + } + + public void testMakeBestStructureGivenTextAndDelimitedOverride() throws Exception { + + // Every line of the text sample has two colons, so colon delimited is possible, just very weird + FileStructureOverrides overrides = FileStructureOverrides.builder() + .setFormat(FileStructure.Format.DELIMITED).setDelimiter(':').build(); + + assertThat(structureFinderManager.makeBestStructureFinder(explanation, TEXT_SAMPLE, StandardCharsets.UTF_8.name(), randomBoolean(), + overrides), instanceOf(DelimitedFileStructureFinder.class)); } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java index ac8f95670ab..8dbfb6a8047 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/FileStructureUtilsTests.java @@ -17,6 +17,7 @@ import java.util.List; import java.util.Map; import java.util.SortedMap; +import static org.elasticsearch.xpack.ml.filestructurefinder.FileStructureOverrides.EMPTY_OVERRIDES; import static org.hamcrest.Matchers.contains; public class FileStructureUtilsTests extends FileStructureTestCase { @@ -32,57 +33,106 @@ public class FileStructureUtilsTests extends FileStructureTestCase { assertFalse(FileStructureUtils.isMoreLikelyTextThanKeyword(randomAlphaOfLengthBetween(1, 256))); } - public void testSingleSampleSingleField() { + public void testGuessTimestampGivenSingleSampleSingleField() { Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample)); + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("field1", match.v1()); assertThat(match.v2().dateFormats, contains("ISO8601")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithSameSingleTimeField() { + public void testGuessTimestampGivenSingleSampleSingleFieldAndConsistentTimeFieldOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("field1").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + Tuple match = + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides); + assertNotNull(match); + assertEquals("field1", match.v1()); + assertThat(match.v2().dateFormats, contains("ISO8601")); + assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndImpossibleTimeFieldOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("field2").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides)); + + assertEquals("Specified timestamp field [field2] is not present in record [{field1=2018-05-24T17:28:31,735}]", e.getMessage()); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndConsistentTimeFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("ISO8601").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + Tuple match = + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides); + assertNotNull(match); + assertEquals("field1", match.v1()); + assertThat(match.v2().dateFormats, contains("ISO8601")); + assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); + } + + public void testGuessTimestampGivenSingleSampleSingleFieldAndImpossibleTimeFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("EEE MMM dd HH:mm:ss YYYY").build(); + + Map sample = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), overrides)); + + assertEquals("Specified timestamp format [EEE MMM dd HH:mm:ss YYYY] does not match for record [{field1=2018-05-24T17:28:31,735}]", + e.getMessage()); + } + + public void testGuessTimestampGivenSamplesWithSameSingleTimeField() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("field1", "2018-05-24T17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("field1", match.v1()); assertThat(match.v2().dateFormats, contains("ISO8601")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithOneSingleTimeFieldDifferentFormat() { + public void testGuessTimestampGivenSamplesWithOneSingleTimeFieldDifferentFormat() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("field1", "2018-05-24 17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithDifferentSingleTimeField() { + public void testGuessTimestampGivenSamplesWithDifferentSingleTimeField() { Map sample1 = Collections.singletonMap("field1", "2018-05-24T17:28:31,735"); Map sample2 = Collections.singletonMap("another_field", "2018-05-24T17:33:39,406"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSingleSampleManyFieldsOneTimeFormat() { + public void testGuessTimestampGivenSingleSampleManyFieldsOneTimeFormat() { Map sample = new LinkedHashMap<>(); sample.put("foo", "not a time"); sample.put("time", "2018-05-24 17:28:31,735"); sample.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample)); + FileStructureUtils.guessTimestampField(explanation, Collections.singletonList(sample), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameSingleTimeFormat() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormat() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -92,14 +142,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "2018-05-29 11:53:02,837"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameTimeFieldDifferentTimeFormat() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameTimeFieldDifferentTimeFormat() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -109,11 +159,11 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "May 29 2018 11:53:02"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithManyFieldsSameSingleTimeFormatDistractionBefore() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormatDistractionBefore() { Map sample1 = new LinkedHashMap<>(); sample1.put("red_herring", "May 29 2007 11:53:02"); sample1.put("time", "2018-05-24 17:28:31,735"); @@ -123,14 +173,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "2018-05-29 11:53:02,837"); sample2.put("bar", 17); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("YYYY-MM-dd HH:mm:ss,SSS")); assertEquals("TIMESTAMP_ISO8601", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsSameSingleTimeFormatDistractionAfter() { + public void testGuessTimestampGivenSamplesWithManyFieldsSameSingleTimeFormatDistractionAfter() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time", "May 24 2018 17:28:31"); @@ -140,14 +190,14 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time", "May 29 2018 11:53:02"); sample2.put("red_herring", "17"); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time", match.v1()); assertThat(match.v2().dateFormats, contains("MMM dd YYYY HH:mm:ss", "MMM d YYYY HH:mm:ss")); assertEquals("CISCOTIMESTAMP", match.v2().grokPatternName); } - public void testSamplesWithManyFieldsInconsistentTimeFields() { + public void testGuessTimestampGivenSamplesWithManyFieldsInconsistentTimeFields() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time1", "May 24 2018 17:28:31"); @@ -157,11 +207,11 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time2", "May 29 2018 11:53:02"); sample2.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNull(match); } - public void testSamplesWithManyFieldsInconsistentAndConsistentTimeFields() { + public void testGuessTimestampGivenSamplesWithManyFieldsInconsistentAndConsistentTimeFields() { Map sample1 = new LinkedHashMap<>(); sample1.put("foo", "not a time"); sample1.put("time1", "2018-05-09 17:28:31,735"); @@ -173,7 +223,7 @@ public class FileStructureUtilsTests extends FileStructureTestCase { sample2.put("time3", "Thu, May 10 2018 11:53:02"); sample2.put("bar", 42); Tuple match = - FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2)); + FileStructureUtils.guessTimestampField(explanation, Arrays.asList(sample1, sample2), EMPTY_OVERRIDES); assertNotNull(match); assertEquals("time2", match.v1()); assertThat(match.v2().dateFormats, contains("MMM dd YYYY HH:mm:ss", "MMM d YYYY HH:mm:ss")); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java index 858709e2764..271e071fc27 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/GrokPatternCreatorTests.java @@ -244,8 +244,7 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { grokPatternCreator.createGrokPatternFromExamples("TIMESTAMP_ISO8601", "timestamp")); assertEquals(5, mappings.size()); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("field")); - assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), - mappings.get("extra_timestamp")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), mappings.get("extra_timestamp")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("field2")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "ip"), mappings.get("ipaddress")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("loglevel")); @@ -273,7 +272,8 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { Map mappings = new HashMap<>(); GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); - assertEquals(new Tuple<>("timestamp", "%{COMBINEDAPACHELOG}"), grokPatternCreator.findFullLineGrokPattern()); + assertEquals(new Tuple<>("timestamp", "%{COMBINEDAPACHELOG}"), + grokPatternCreator.findFullLineGrokPattern(randomBoolean() ? "timestamp" : null)); assertEquals(10, mappings.size()); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "text"), mappings.get("agent")); assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("auth")); @@ -323,4 +323,59 @@ public class GrokPatternCreatorTests extends FileStructureTestCase { assertEquals("", grokPatternCreator.getOverallGrokPatternBuilder().toString()); assertSame(snippets, adjustedSnippets); } + + public void testValidateFullLineGrokPatternGivenValid() { + + String timestampField = "utc_timestamp"; + String grokPattern = "%{INT:serial_no}\\t%{TIMESTAMP_ISO8601:local_timestamp}\\t%{TIMESTAMP_ISO8601:utc_timestamp}\\t" + + "%{INT:user_id}\\t%{HOSTNAME:host}\\t%{IP:client_ip}\\t%{WORD:method}\\t%{LOGLEVEL:severity}\\t%{PROG:program}\\t" + + "%{GREEDYDATA:message}"; + + // Two timestamps: one local, one UTC + Collection sampleMessages = Arrays.asList( + "559550912540598297\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t38545844\tserv02nw07\t192.168.114.28\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912548986880\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t9049724\tserv02nw03\t10.120.48.147\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912548986887\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t884343\tserv02tw03\t192.168.121.189\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp", + "559550912603512850\t2016-04-20T14:06:53\t2016-04-20T21:06:53Z\t8907014\tserv02nw01\t192.168.118.208\tAuthpriv\t" + + "Info\tsshd\tsubsystem request for sftp"); + + Map mappings = new HashMap<>(); + GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); + + grokPatternCreator.validateFullLineGrokPattern(grokPattern, timestampField); + assertEquals(9, mappings.size()); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("serial_no")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "date"), mappings.get("local_timestamp")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "long"), mappings.get("user_id")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("host")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "ip"), mappings.get("client_ip")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("method")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("program")); + assertEquals(Collections.singletonMap(FileStructureUtils.MAPPING_TYPE_SETTING, "keyword"), mappings.get("message")); + } + + public void testValidateFullLineGrokPatternGivenInvalid() { + + String timestampField = "utc_timestamp"; + String grokPattern = "%{INT:serial_no}\\t%{TIMESTAMP_ISO8601:local_timestamp}\\t%{TIMESTAMP_ISO8601:utc_timestamp}\\t" + + "%{INT:user_id}\\t%{HOSTNAME:host}\\t%{IP:client_ip}\\t%{WORD:method}\\t%{LOGLEVEL:severity}\\t%{PROG:program}\\t" + + "%{GREEDYDATA:message}"; + + Collection sampleMessages = Arrays.asList( + "Sep 8 11:55:06 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'elastic.slack.com/A/IN': 95.110.64.205#53", + "Sep 8 11:55:08 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'slack-imgs.com/A/IN': 95.110.64.205#53", + "Sep 8 11:55:35 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'www.elastic.co/A/IN': 95.110.68.206#53", + "Sep 8 11:55:42 linux named[22529]: error (unexpected RCODE REFUSED) resolving 'b.akamaiedge.net/A/IN': 95.110.64.205#53"); + + Map mappings = new HashMap<>(); + GrokPatternCreator grokPatternCreator = new GrokPatternCreator(explanation, sampleMessages, mappings, null); + + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> grokPatternCreator.validateFullLineGrokPattern(grokPattern, timestampField)); + + assertEquals("Supplied Grok pattern [" + grokPattern + "] does not match sample messages", e.getMessage()); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java index f41868be862..6856e9a6021 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/JsonFileStructureFinderTests.java @@ -18,7 +18,8 @@ public class JsonFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, JSON_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, JSON_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -32,6 +33,7 @@ public class JsonFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertNull(structure.getMultilineStartPattern()); assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertNull(structure.getGrokPattern()); diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java index a23080a8272..5bc40a16511 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/TextLogFileStructureFinderTests.java @@ -15,6 +15,90 @@ import java.util.Set; public class TextLogFileStructureFinderTests extends FileStructureTestCase { + private static final String EXCEPTION_TRACE_SAMPLE = + "[2018-02-28T14:49:40,517][DEBUG][o.e.a.b.TransportShardBulkAction] [an_index][2] failed to execute bulk item " + + "(index) BulkShardRequest [[an_index][2]] containing [33] requests\n" + + "java.lang.IllegalArgumentException: Document contains at least one immense term in field=\"message.keyword\" (whose UTF8 " + + "encoding is longer than the max length 32766), all of which were skipped. Please correct the analyzer to not produce " + + "such terms. The prefix of the first immense term is: '[60, 83, 79, 65, 80, 45, 69, 78, 86, 58, 69, 110, 118, 101, 108, " + + "111, 112, 101, 32, 120, 109, 108, 110, 115, 58, 83, 79, 65, 80, 45]...', original message: bytes can be at most 32766 " + + "in length; got 49023\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain$PerField.invert(DefaultIndexingChain.java:796) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain.processField(DefaultIndexingChain.java:430) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DefaultIndexingChain.processDocument(DefaultIndexingChain.java:392) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DocumentsWriterPerThread.updateDocument(DocumentsWriterPerThread.java:240) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.DocumentsWriter.updateDocument(DocumentsWriter.java:496) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.IndexWriter.updateDocument(IndexWriter.java:1729) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1464) " + + "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:1070) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.indexIntoLucene(InternalEngine.java:1012) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:878) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.index(IndexShard.java:738) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperation(IndexShard.java:707) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperationOnPrimary(IndexShard.java:673) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequestOnPrimary(TransportShardBulkAction.java:548) " + + "~[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequest(TransportShardBulkAction.java:140) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeBulkItemRequest(TransportShardBulkAction.java:236) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.performOnPrimary(TransportShardBulkAction.java:123) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:110) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:72) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + + "(TransportReplicationAction.java:1034) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + + "(TransportReplicationAction.java:1012) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.ReplicationOperation.execute(ReplicationOperation.java:103) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + + "(TransportReplicationAction.java:359) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + + "(TransportReplicationAction.java:299) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + + "(TransportReplicationAction.java:975) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + + "(TransportReplicationAction.java:972) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShardOperationPermits.acquire(IndexShardOperationPermits.java:238) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.index.shard.IndexShard.acquirePrimaryOperationPermit(IndexShard.java:2220) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.acquirePrimaryShardReference" + + "(TransportReplicationAction.java:984) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.access$500(TransportReplicationAction.java:98) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.doRun" + + "(TransportReplicationAction.java:320) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + + ".messageReceived(TransportReplicationAction.java:295) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + + ".messageReceived(TransportReplicationAction.java:282) [elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:66) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.transport.TransportService$7.doRun(TransportService.java:656) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:635) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + + "[elasticsearch-6.2.1.jar:6.2.1]\n" + + "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_144]\n" + + "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_144]\n" + + "\tat java.lang.Thread.run(Thread.java:748) [?:1.8.0_144]\n"; + private FileStructureFinderFactory factory = new TextLogFileStructureFinderFactory(); public void testCreateConfigsGivenElasticsearchLog() throws Exception { @@ -22,7 +106,8 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -36,6 +121,7 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); assertNull(structure.getHasHeaderRow()); assertNull(structure.getShouldTrimFields()); assertEquals("\\[%{TIMESTAMP_ISO8601:timestamp}\\]\\[%{LOGLEVEL:loglevel} \\]\\[.*", structure.getGrokPattern()); @@ -43,6 +129,85 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); } + public void testCreateConfigsGivenElasticsearchLogAndTimestampFieldOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampField("my_time").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.SEMI_STRUCTURED_TEXT, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); + assertNull(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals("\\[%{TIMESTAMP_ISO8601:my_time}\\]\\[%{LOGLEVEL:loglevel} \\]\\[.*", structure.getGrokPattern()); + assertEquals("my_time", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenElasticsearchLogAndGrokPatternOverride() throws Exception { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setGrokPattern("\\[%{TIMESTAMP_ISO8601:timestamp}\\]" + + "\\[%{LOGLEVEL:loglevel} *\\]\\[%{JAVACLASS:class} *\\] \\[%{HOSTNAME:node}\\] %{JAVALOGMESSAGE:message}").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + FileStructureFinder structureFinder = factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides); + + FileStructure structure = structureFinder.getStructure(); + + assertEquals(FileStructure.Format.SEMI_STRUCTURED_TEXT, structure.getFormat()); + assertEquals(charset, structure.getCharset()); + if (hasByteOrderMarker == null) { + assertNull(structure.getHasByteOrderMarker()); + } else { + assertEquals(hasByteOrderMarker, structure.getHasByteOrderMarker()); + } + assertNull(structure.getExcludeLinesPattern()); + assertEquals("^\\[\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", structure.getMultilineStartPattern()); + assertNull(structure.getDelimiter()); + assertNull(structure.getQuote()); + assertNull(structure.getHasHeaderRow()); + assertNull(structure.getShouldTrimFields()); + assertEquals("\\[%{TIMESTAMP_ISO8601:timestamp}\\]\\[%{LOGLEVEL:loglevel} *\\]" + + "\\[%{JAVACLASS:class} *\\] \\[%{HOSTNAME:node}\\] %{JAVALOGMESSAGE:message}", structure.getGrokPattern()); + assertEquals("timestamp", structure.getTimestampField()); + assertEquals(Collections.singletonList("ISO8601"), structure.getTimestampFormats()); + } + + public void testCreateConfigsGivenElasticsearchLogAndImpossibleGrokPatternOverride() { + + // This Grok pattern cannot be matched against the messages in the sample because the fields are in the wrong order + FileStructureOverrides overrides = FileStructureOverrides.builder().setGrokPattern("\\[%{LOGLEVEL:loglevel} *\\]" + + "\\[%{HOSTNAME:node}\\]\\[%{TIMESTAMP_ISO8601:timestamp}\\] \\[%{JAVACLASS:class} *\\] %{JAVALOGMESSAGE:message}").build(); + + assertTrue(factory.canCreateFromSample(explanation, TEXT_SAMPLE)); + + String charset = randomFrom(POSSIBLE_CHARSETS); + Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); + IllegalArgumentException e = expectThrows(IllegalArgumentException.class, + () -> factory.createFromSample(explanation, TEXT_SAMPLE, charset, hasByteOrderMarker, overrides)); + + assertEquals("Supplied Grok pattern [\\[%{LOGLEVEL:loglevel} *\\]\\[%{HOSTNAME:node}\\]\\[%{TIMESTAMP_ISO8601:timestamp}\\] " + + "\\[%{JAVACLASS:class} *\\] %{JAVALOGMESSAGE:message}] does not match sample messages", e.getMessage()); + } + public void testCreateMultiLineMessageStartRegexGivenNoPrefaces() { for (TimestampFormatFinder.CandidateTimestampFormat candidateTimestampFormat : TimestampFormatFinder.ORDERED_CANDIDATE_FORMATS) { String simpleDateRegex = candidateTimestampFormat.simplePattern.pattern(); @@ -144,97 +309,17 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { "[2018-06-27T11:59:23,588][INFO ][o.e.p.PluginsService ] [node-0] loaded module [x-pack-watcher]\n" + "[2018-06-27T11:59:23,588][INFO ][o.e.p.PluginsService ] [node-0] no plugins loaded\n"; - Tuple> mostLikelyMatch = TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n")); + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n"), FileStructureOverrides.EMPTY_OVERRIDES); assertNotNull(mostLikelyMatch); assertEquals(new TimestampMatch(7, "", "ISO8601", "\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", ""), mostLikelyMatch.v1()); } public void testMostLikelyTimestampGivenExceptionTrace() { - String sample = "[2018-02-28T14:49:40,517][DEBUG][o.e.a.b.TransportShardBulkAction] [an_index][2] failed to execute bulk item " + - "(index) BulkShardRequest [[an_index][2]] containing [33] requests\n" + - "java.lang.IllegalArgumentException: Document contains at least one immense term in field=\"message.keyword\" (whose UTF8 " + - "encoding is longer than the max length 32766), all of which were skipped. Please correct the analyzer to not produce " + - "such terms. The prefix of the first immense term is: '[60, 83, 79, 65, 80, 45, 69, 78, 86, 58, 69, 110, 118, 101, 108, " + - "111, 112, 101, 32, 120, 109, 108, 110, 115, 58, 83, 79, 65, 80, 45]...', original message: bytes can be at most 32766 " + - "in length; got 49023\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain$PerField.invert(DefaultIndexingChain.java:796) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain.processField(DefaultIndexingChain.java:430) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DefaultIndexingChain.processDocument(DefaultIndexingChain.java:392) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DocumentsWriterPerThread.updateDocument(DocumentsWriterPerThread.java:240) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.DocumentsWriter.updateDocument(DocumentsWriter.java:496) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.IndexWriter.updateDocument(IndexWriter.java:1729) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.apache.lucene.index.IndexWriter.addDocument(IndexWriter.java:1464) " + - "~[lucene-core-7.2.1.jar:7.2.1 b2b6438b37073bee1fca40374e85bf91aa457c0b - ubuntu - 2018-01-10 00:48:43]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:1070) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.indexIntoLucene(InternalEngine.java:1012) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.engine.InternalEngine.index(InternalEngine.java:878) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.index(IndexShard.java:738) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperation(IndexShard.java:707) ~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.applyIndexOperationOnPrimary(IndexShard.java:673) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequestOnPrimary(TransportShardBulkAction.java:548) " + - "~[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeIndexRequest(TransportShardBulkAction.java:140) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.executeBulkItemRequest(TransportShardBulkAction.java:236) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.performOnPrimary(TransportShardBulkAction.java:123) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:110) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.bulk.TransportShardBulkAction.shardOperationOnPrimary(TransportShardBulkAction.java:72) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + - "(TransportReplicationAction.java:1034) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryShardReference.perform" + - "(TransportReplicationAction.java:1012) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.ReplicationOperation.execute(ReplicationOperation.java:103) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + - "(TransportReplicationAction.java:359) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.onResponse" + - "(TransportReplicationAction.java:299) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + - "(TransportReplicationAction.java:975) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$1.onResponse" + - "(TransportReplicationAction.java:972) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShardOperationPermits.acquire(IndexShardOperationPermits.java:238) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.index.shard.IndexShard.acquirePrimaryOperationPermit(IndexShard.java:2220) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.acquirePrimaryShardReference" + - "(TransportReplicationAction.java:984) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction.access$500(TransportReplicationAction.java:98) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$AsyncPrimaryAction.doRun" + - "(TransportReplicationAction.java:320) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + - ".messageReceived(TransportReplicationAction.java:295) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.action.support.replication.TransportReplicationAction$PrimaryOperationTransportHandler" + - ".messageReceived(TransportReplicationAction.java:282) [elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.transport.RequestHandlerRegistry.processMessageReceived(RequestHandlerRegistry.java:66) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.transport.TransportService$7.doRun(TransportService.java:656) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.ThreadContext$ContextPreservingAbstractRunnable.doRun(ThreadContext.java:635) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat org.elasticsearch.common.util.concurrent.AbstractRunnable.run(AbstractRunnable.java:37) " + - "[elasticsearch-6.2.1.jar:6.2.1]\n" + - "\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) [?:1.8.0_144]\n" + - "\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) [?:1.8.0_144]\n" + - "\tat java.lang.Thread.run(Thread.java:748) [?:1.8.0_144]\n"; - Tuple> mostLikelyMatch = TextLogFileStructureFinder.mostLikelyTimestamp(sample.split("\n")); + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), FileStructureOverrides.EMPTY_OVERRIDES); assertNotNull(mostLikelyMatch); // Even though many lines have a timestamp near the end (in the Lucene version information), @@ -243,4 +328,26 @@ public class TextLogFileStructureFinderTests extends FileStructureTestCase { assertEquals(new TimestampMatch(7, "", "ISO8601", "\\b\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", ""), mostLikelyMatch.v1()); } + + public void testMostLikelyTimestampGivenExceptionTraceAndTimestampFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("YYYY-MM-dd HH:mm:ss").build(); + + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), overrides); + assertNotNull(mostLikelyMatch); + + // The override should force the seemingly inferior choice of timestamp + assertEquals(new TimestampMatch(6, "", "YYYY-MM-dd HH:mm:ss", "\\b\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}", "TIMESTAMP_ISO8601", + ""), mostLikelyMatch.v1()); + } + + public void testMostLikelyTimestampGivenExceptionTraceAndImpossibleTimestampFormatOverride() { + + FileStructureOverrides overrides = FileStructureOverrides.builder().setTimestampFormat("MMM dd HH:mm:ss").build(); + + Tuple> mostLikelyMatch = + TextLogFileStructureFinder.mostLikelyTimestamp(EXCEPTION_TRACE_SAMPLE.split("\n"), overrides); + assertNull(mostLikelyMatch); + } } diff --git a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java index 4bf65ba7835..01c44147b04 100644 --- a/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java +++ b/x-pack/plugin/ml/src/test/java/org/elasticsearch/xpack/ml/filestructurefinder/XmlFileStructureFinderTests.java @@ -18,7 +18,8 @@ public class XmlFileStructureFinderTests extends FileStructureTestCase { String charset = randomFrom(POSSIBLE_CHARSETS); Boolean hasByteOrderMarker = randomHasByteOrderMarker(charset); - FileStructureFinder structureFinder = factory.createFromSample(explanation, XML_SAMPLE, charset, hasByteOrderMarker); + FileStructureFinder structureFinder = factory.createFromSample(explanation, XML_SAMPLE, charset, hasByteOrderMarker, + FileStructureOverrides.EMPTY_OVERRIDES); FileStructure structure = structureFinder.getStructure(); @@ -32,6 +33,7 @@ public class XmlFileStructureFinderTests extends FileStructureTestCase { assertNull(structure.getExcludeLinesPattern()); assertEquals("^\\s* Date: Wed, 12 Sep 2018 17:05:00 -0400 Subject: [PATCH 29/45] Adjust BWC version on settings upgrade test (#33650) The skip_unavailable setting did not exist until 6.1.0. This means that we need to skip this test on versions prior to 6.1.0. We need to use this setting because otherwise we will fail startup without it (since we are not setting up a real remote cluster connection). This commit adds a skip for all versions prior to 6.1.0. --- .../upgrades/FullClusterRestartSettingsUpgradeIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java index 5e3ccc75b74..99afdce59c5 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -44,6 +44,7 @@ import static org.hamcrest.Matchers.equalTo; public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { public void testRemoteClusterSettingsUpgraded() throws IOException { + assumeTrue("skip_unavailable did not exist until 6.1.0", getOldClusterVersion().onOrAfter(Version.V_6_1_0)); assumeTrue("settings automatically upgraded since 6.5.0", getOldClusterVersion().before(Version.V_6_5_0)); if (isRunningAgainstOldCluster()) { final Request putSettingsRequest = new Request("PUT", "/_cluster/settings"); From f7c131f118a0408744578e2c38aea3389429c728 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 14 Sep 2018 05:55:46 -0400 Subject: [PATCH 30/45] Revert "Mute FullClusterRestartSettingsUpgradeIT" This reverts commit e9826164bddf0263f0a826e6d2824d5f103bb5b4. --- .../upgrades/FullClusterRestartSettingsUpgradeIT.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java index 99afdce59c5..fb14b89fc62 100644 --- a/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java +++ b/qa/full-cluster-restart/src/test/java/org/elasticsearch/upgrades/FullClusterRestartSettingsUpgradeIT.java @@ -19,7 +19,6 @@ package org.elasticsearch.upgrades; -import org.apache.lucene.util.LuceneTestCase.AwaitsFix; import org.elasticsearch.Version; import org.elasticsearch.action.admin.cluster.settings.ClusterGetSettingsResponse; import org.elasticsearch.client.Request; @@ -40,7 +39,6 @@ import static org.elasticsearch.transport.RemoteClusterAware.SEARCH_REMOTE_CLUST import static org.elasticsearch.transport.RemoteClusterService.SEARCH_REMOTE_CLUSTER_SKIP_UNAVAILABLE; import static org.hamcrest.Matchers.equalTo; -@AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33697") public class FullClusterRestartSettingsUpgradeIT extends AbstractFullClusterRestartTestCase { public void testRemoteClusterSettingsUpgraded() throws IOException { From b8fb83d7a4663e7ed45a2f42ada826766f956f78 Mon Sep 17 00:00:00 2001 From: Igor Motov Date: Fri, 14 Sep 2018 14:17:27 +0400 Subject: [PATCH 31/45] Mute ClusterDisruptionIT#testSendingShardFailure Tracked by #33704 --- .../java/org/elasticsearch/discovery/ClusterDisruptionIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java b/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java index fab38a2b73b..33b6ffb9a75 100644 --- a/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java +++ b/server/src/test/java/org/elasticsearch/discovery/ClusterDisruptionIT.java @@ -288,6 +288,7 @@ public class ClusterDisruptionIT extends AbstractDisruptionTestCase { } // simulate handling of sending shard failure during an isolation + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/33704") public void testSendingShardFailure() throws Exception { List nodes = startCluster(3, 2); String masterNode = internalCluster().getMasterName(); From 222f42274ec7a8686e58fe4b7acff38a438a0776 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 14 Sep 2018 13:28:11 +0200 Subject: [PATCH 32/45] [CCR] Check whether the rejected execution exception has the shutdown flag set (#33703) and if so debug log it and otherwise rethrow. This should fix a couple of test failures where during test teardown tests failed due to uncaught exceptions being detected. --- .../xpack/ccr/action/ShardFollowTasksExecutor.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java index 7b63e73ee59..86556480567 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowTasksExecutor.java @@ -23,6 +23,7 @@ import org.elasticsearch.cluster.metadata.MappingMetaData; import org.elasticsearch.cluster.routing.IndexRoutingTable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; +import org.elasticsearch.common.util.concurrent.EsRejectedExecutionException; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; @@ -90,8 +91,17 @@ public class ShardFollowTasksExecutor extends PersistentTasksExecutor scheduler = - (delay, command) -> threadPool.schedule(delay, Ccr.CCR_THREAD_POOL_NAME, command); + BiConsumer scheduler = (delay, command) -> { + try { + threadPool.schedule(delay, Ccr.CCR_THREAD_POOL_NAME, command); + } catch (EsRejectedExecutionException e) { + if (e.isExecutorShutdown()) { + logger.debug("couldn't schedule command, executor is shutting down", e); + } else { + throw e; + } + } + }; return new ShardFollowNodeTask( id, type, action, getDescription(taskInProgress), parentTaskId, headers, params, scheduler, System::nanoTime) { From faa3c1624136ba075c737bce312f570564b9ba35 Mon Sep 17 00:00:00 2001 From: Alexander Reelsen Date: Fri, 14 Sep 2018 13:55:16 +0200 Subject: [PATCH 33/45] Core: Add DateFormatter interface for java time parsing (#33467) The existing approach used date formatters when a format based string like `date_time||epoch_millis` was used, instead of the custom code. In order to properly solve this, a new interface called `DateFormatter` has been added, which now can be implemented for custom formatters. Currently there are two implementations, one using java time and one doing the epoch_millis formatter, which simply parses a number and then converts it to a date in UTC timezone. The DateFormatter interface now also has a method to retrieve the name of the formatter pattern, which is needed for mapping changes anyway. The existing `CompoundDateTimeFormatter` class has been removed, the name was not really nice anyway. One more minor change is the fact, that the new java time using FormatDateFormatter does not try to parse the date with its printer implementation first (which might be a strict one and fail), but a printer can now be specified in addition. This saves one potential failure/exception when parsing less strict dates. If only a printer is specified, the printer will also be used as a parser. --- .../cluster/metadata/IndexGraveyard.java | 5 +- .../cluster/routing/UnassignedInfo.java | 5 +- .../java/org/elasticsearch/common/Table.java | 4 +- .../common/time/DateFormatter.java | 133 +++++++ .../common/time/DateFormatters.java | 341 +++++++++--------- .../common/time/DateMathParser.java | 8 +- .../common/time/EpochMillisDateFormatter.java | 73 ++++ ...eFormatter.java => JavaDateFormatter.java} | 77 ++-- .../XContentElasticsearchExtension.java | 8 +- .../elasticsearch/monitor/jvm/HotThreads.java | 4 +- .../rest/action/cat/RestSnapshotAction.java | 4 +- .../rest/action/cat/RestTasksAction.java | 4 +- .../elasticsearch/snapshots/SnapshotInfo.java | 4 +- .../joda/JavaJodaTimeDuellingTests.java | 6 +- .../common/time/DateFormattersTests.java | 19 +- .../common/time/DateMathParserTests.java | 6 +- .../bucket/histogram/DateHistogramTests.java | 4 +- 17 files changed, 456 insertions(+), 249 deletions(-) create mode 100644 server/src/main/java/org/elasticsearch/common/time/DateFormatter.java create mode 100644 server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java rename server/src/main/java/org/elasticsearch/common/time/{CompoundDateTimeFormatter.java => JavaDateFormatter.java} (59%) diff --git a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java index 8d0ad8efb7f..7a43ce31d33 100644 --- a/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java +++ b/server/src/main/java/org/elasticsearch/cluster/metadata/IndexGraveyard.java @@ -28,7 +28,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.xcontent.ContextParser; import org.elasticsearch.common.xcontent.ObjectParser; @@ -368,8 +368,7 @@ public final class IndexGraveyard implements MetaData.Custom { TOMBSTONE_PARSER.declareString((b, s) -> {}, new ParseField(DELETE_DATE_KEY)); } - static final CompoundDateTimeFormatter FORMATTER = - DateFormatters.forPattern("strict_date_optional_time").withZone(ZoneOffset.UTC); + static final DateFormatter FORMATTER = DateFormatters.forPattern("strict_date_optional_time").withZone(ZoneOffset.UTC); static ContextParser getParser() { return (parser, context) -> TOMBSTONE_PARSER.apply(parser, null).build(); diff --git a/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java b/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java index ad715500a9e..21885d1788c 100644 --- a/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java +++ b/server/src/main/java/org/elasticsearch/cluster/routing/UnassignedInfo.java @@ -31,7 +31,7 @@ import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ToXContentFragment; @@ -48,8 +48,7 @@ import java.util.Objects; */ public final class UnassignedInfo implements ToXContentFragment, Writeable { - public static final CompoundDateTimeFormatter DATE_TIME_FORMATTER = - DateFormatters.forPattern("dateOptionalTime").withZone(ZoneOffset.UTC); + public static final DateFormatter DATE_TIME_FORMATTER = DateFormatters.forPattern("dateOptionalTime").withZone(ZoneOffset.UTC); public static final Setting INDEX_DELAYED_NODE_LEFT_TIMEOUT_SETTING = Setting.positiveTimeSetting("index.unassigned.node_left.delayed_timeout", TimeValue.timeValueMinutes(1), Property.Dynamic, diff --git a/server/src/main/java/org/elasticsearch/common/Table.java b/server/src/main/java/org/elasticsearch/common/Table.java index 13d13066e16..a41fd267329 100644 --- a/server/src/main/java/org/elasticsearch/common/Table.java +++ b/server/src/main/java/org/elasticsearch/common/Table.java @@ -19,7 +19,7 @@ package org.elasticsearch.common; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import java.time.Instant; @@ -85,7 +85,7 @@ public class Table { return this; } - private static final CompoundDateTimeFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); + private static final DateFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); public Table startRow() { if (headers.isEmpty()) { diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java new file mode 100644 index 00000000000..d16662b23b9 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatter.java @@ -0,0 +1,133 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.common.time; + +import java.time.ZoneId; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; + +public interface DateFormatter { + + /** + * Try to parse input to a java time TemporalAccessor + * @param input An arbitrary string resembling the string representation of a date or time + * @throws DateTimeParseException If parsing fails, this exception will be thrown. + * Note that it can contained suppressed exceptions when several formatters failed parse this value + * @return The java time object containing the parsed input + */ + TemporalAccessor parse(String input); + + /** + * Create a copy of this formatter that is configured to parse dates in the specified time zone + * + * @param zoneId The time zone to act on + * @return A copy of the date formatter this has been called on + */ + DateFormatter withZone(ZoneId zoneId); + + /** + * Print the supplied java time accessor in a string based representation according to this formatter + * + * @param accessor The temporal accessor used to format + * @return The string result for the formatting + */ + String format(TemporalAccessor accessor); + + /** + * A name based format for this formatter. Can be one of the registered formatters like epoch_millis or + * a configured format like HH:mm:ss + * + * @return The name of this formatter + */ + String pattern(); + + /** + * Configure a formatter using default fields for a TemporalAccessor that should be used in case + * the supplied date is not having all of those fields + * + * @param fields A Map<TemporalField, Long> of fields to be used as fallbacks + * @return A new date formatter instance, that will use those fields during parsing + */ + DateFormatter parseDefaulting(Map fields); + + /** + * Merge several date formatters into a single one. Useful if you need to have several formatters with + * different formats act as one, for example when you specify a + * format like date_hour||epoch_millis + * + * @param formatters The list of date formatters to be merged together + * @return The new date formtter containing the specified date formatters + */ + static DateFormatter merge(DateFormatter ... formatters) { + return new MergedDateFormatter(formatters); + } + + class MergedDateFormatter implements DateFormatter { + + private final String format; + private final DateFormatter[] formatters; + + MergedDateFormatter(DateFormatter ... formatters) { + this.formatters = formatters; + this.format = Arrays.stream(formatters).map(DateFormatter::pattern).collect(Collectors.joining("||")); + } + + @Override + public TemporalAccessor parse(String input) { + DateTimeParseException failure = null; + for (DateFormatter formatter : formatters) { + try { + return formatter.parse(input); + } catch (DateTimeParseException e) { + if (failure == null) { + failure = e; + } else { + failure.addSuppressed(e); + } + } + } + throw failure; + } + + @Override + public DateFormatter withZone(ZoneId zoneId) { + return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.withZone(zoneId)).toArray(DateFormatter[]::new)); + } + + @Override + public String format(TemporalAccessor accessor) { + return formatters[0].format(accessor); + } + + @Override + public String pattern() { + return format; + } + + @Override + public DateFormatter parseDefaulting(Map fields) { + return new MergedDateFormatter(Arrays.stream(formatters).map(f -> f.parseDefaulting(fields)).toArray(DateFormatter[]::new)); + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java index 69b8cb0c85b..5f687651344 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateFormatters.java @@ -25,12 +25,10 @@ import java.time.DateTimeException; import java.time.DayOfWeek; import java.time.Instant; import java.time.LocalDate; -import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; -import java.time.format.DateTimeParseException; import java.time.format.ResolverStyle; import java.time.format.SignStyle; import java.time.temporal.ChronoField; @@ -38,9 +36,6 @@ import java.time.temporal.IsoFields; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalAdjusters; import java.time.temporal.WeekFields; -import java.util.Arrays; -import java.util.Collection; -import java.util.LinkedHashSet; import java.util.Locale; import static java.time.temporal.ChronoField.DAY_OF_MONTH; @@ -106,8 +101,9 @@ public class DateFormatters { /** * Returns a generic ISO datetime parser where the date is mandatory and the time is optional. */ - private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME = - new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2); + private static final DateFormatter STRICT_DATE_OPTIONAL_TIME = + new JavaDateFormatter("strict_date_optional_time", STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, + STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_2); private static final DateTimeFormatter STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1 = new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) @@ -140,8 +136,9 @@ public class DateFormatters { /** * Returns a generic ISO datetime parser where the date is mandatory and the time is optional with nanosecond resolution. */ - private static final CompoundDateTimeFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = - new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2); + private static final DateFormatter STRICT_DATE_OPTIONAL_TIME_NANOS = new JavaDateFormatter("strict_date_optional_time_nanos", + STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, + STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_1, STRICT_DATE_OPTIONAL_TIME_FORMATTER_WITH_NANOS_2); ///////////////////////////////////////// // @@ -162,7 +159,8 @@ public class DateFormatters { * Returns a basic formatter for a two digit hour of day, two digit minute * of hour, two digit second of minute, and time zone offset (HHmmssZ). */ - private static final CompoundDateTimeFormatter BASIC_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_TIME_NO_MILLIS = new JavaDateFormatter("basic_time_no_millis", + new DateTimeFormatterBuilder().append(BASIC_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_TIME_NO_MILLIS_BASE).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -186,7 +184,7 @@ public class DateFormatters { * of hour, two digit second of minute, three digit millis, and time zone * offset (HHmmss.SSSZ). */ - private static final CompoundDateTimeFormatter BASIC_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_TIME = new JavaDateFormatter("basic_time", new DateTimeFormatterBuilder().append(BASIC_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_TIME_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_TIME_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) @@ -203,7 +201,7 @@ public class DateFormatters { * of hour, two digit second of minute, three digit millis, and time zone * offset prefixed by 'T' ('T'HHmmss.SSSZ). */ - private static final CompoundDateTimeFormatter BASIC_T_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_T_TIME = new JavaDateFormatter("basic_t_time", new DateTimeFormatterBuilder().append(BASIC_T_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_T_TIME_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_T_TIME_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) @@ -214,11 +212,11 @@ public class DateFormatters { * of hour, two digit second of minute, and time zone offset prefixed by 'T' * ('T'HHmmssZ). */ - private static final CompoundDateTimeFormatter BASIC_T_TIME_NO_MILLIS = new CompoundDateTimeFormatter( - new DateTimeFormatterBuilder().appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) - .appendZoneOrOffsetId().toFormatter(Locale.ROOT), - new DateTimeFormatterBuilder().appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) - .append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) + private static final DateFormatter BASIC_T_TIME_NO_MILLIS = new JavaDateFormatter("basic_t_time_no_millis", + new DateTimeFormatterBuilder().appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), + new DateTimeFormatterBuilder().appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), + new DateTimeFormatterBuilder().appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE).append(TIME_ZONE_FORMATTER_NO_COLON) + .toFormatter(Locale.ROOT) ); private static final DateTimeFormatter BASIC_YEAR_MONTH_DAY_FORMATTER = new DateTimeFormatterBuilder() @@ -241,7 +239,7 @@ public class DateFormatters { * Returns a basic formatter that combines a basic date and time, separated * by a 'T' (yyyyMMdd'T'HHmmss.SSSZ). */ - private static final CompoundDateTimeFormatter BASIC_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_DATE_TIME = new JavaDateFormatter("basic_date_time", new DateTimeFormatterBuilder().append(BASIC_DATE_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_DATE_TIME_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_DATE_TIME_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) @@ -254,7 +252,9 @@ public class DateFormatters { * Returns a basic formatter that combines a basic date and time without millis, * separated by a 'T' (yyyyMMdd'T'HHmmssZ). */ - private static final CompoundDateTimeFormatter BASIC_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_DATE_TIME_NO_MILLIS = new JavaDateFormatter("basic_t_time_no_millis", + new DateTimeFormatterBuilder().append(BASIC_DATE_T).append(BASIC_TIME_NO_MILLIS_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_DATE_T).append(BASIC_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_DATE_T).append(BASIC_TIME_NO_MILLIS_BASE) @@ -265,14 +265,14 @@ public class DateFormatters { * Returns a formatter for a full ordinal date, using a four * digit year and three digit dayOfYear (yyyyDDD). */ - private static final CompoundDateTimeFormatter BASIC_ORDINAL_DATE = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_ORDINAL_DATE = new JavaDateFormatter("basic_ordinal_date", DateTimeFormatter.ofPattern("yyyyDDD", Locale.ROOT)); /* * Returns a formatter for a full ordinal date and time, using a four * digit year and three digit dayOfYear (yyyyDDD'T'HHmmss.SSSZ). */ - private static final CompoundDateTimeFormatter BASIC_ORDINAL_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_ORDINAL_DATE_TIME = new JavaDateFormatter("basic_ordinal_date_time", new DateTimeFormatterBuilder().appendPattern("yyyyDDD").append(BASIC_T_TIME_PRINTER) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendPattern("yyyyDDD").append(BASIC_T_TIME_FORMATTER) @@ -284,7 +284,9 @@ public class DateFormatters { * Returns a formatter for a full ordinal date and time without millis, * using a four digit year and three digit dayOfYear (yyyyDDD'T'HHmmssZ). */ - private static final CompoundDateTimeFormatter BASIC_ORDINAL_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_ORDINAL_DATE_TIME_NO_MILLIS = new JavaDateFormatter("basic_ordinal_date_time_no_millis", + new DateTimeFormatterBuilder().appendPattern("yyyyDDD").appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendPattern("yyyyDDD").appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendPattern("yyyyDDD").appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) @@ -329,14 +331,14 @@ public class DateFormatters { * Returns a basic formatter for a full date as four digit weekyear, two * digit week of weekyear, and one digit day of week (xxxx'W'wwe). */ - private static final CompoundDateTimeFormatter STRICT_BASIC_WEEK_DATE = - new CompoundDateTimeFormatter(STRICT_BASIC_WEEK_DATE_PRINTER, STRICT_BASIC_WEEK_DATE_FORMATTER); + private static final DateFormatter STRICT_BASIC_WEEK_DATE = + new JavaDateFormatter("strict_basic_week_date", STRICT_BASIC_WEEK_DATE_PRINTER, STRICT_BASIC_WEEK_DATE_FORMATTER); /* * Returns a basic formatter that combines a basic weekyear date and time * without millis, separated by a 'T' (xxxx'W'wwe'T'HHmmssX). */ - private static final CompoundDateTimeFormatter STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter("strict_basic_week_date_no_millis", new DateTimeFormatterBuilder() .append(STRICT_BASIC_WEEK_DATE_PRINTER).append(DateTimeFormatter.ofPattern("'T'HHmmssX", Locale.ROOT)) .toFormatter(Locale.ROOT), @@ -349,7 +351,7 @@ public class DateFormatters { * Returns a basic formatter that combines a basic weekyear date and time, * separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX). */ - private static final CompoundDateTimeFormatter STRICT_BASIC_WEEK_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_BASIC_WEEK_DATE_TIME = new JavaDateFormatter("strict_basic_week_date_time", new DateTimeFormatterBuilder() .append(STRICT_BASIC_WEEK_DATE_PRINTER) .append(DateTimeFormatter.ofPattern("'T'HHmmss.SSSX", Locale.ROOT)) @@ -363,30 +365,32 @@ public class DateFormatters { /* * An ISO date formatter that formats or parses a date without an offset, such as '2011-12-03'. */ - private static final CompoundDateTimeFormatter STRICT_DATE = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE = new JavaDateFormatter("strict_date", DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.LENIENT)); /* * A date formatter that formats or parses a date plus an hour without an offset, such as '2011-12-03T01'. */ - private static final CompoundDateTimeFormatter STRICT_DATE_HOUR = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE_HOUR = new JavaDateFormatter("strict_date_hour", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH", Locale.ROOT)); /* * A date formatter that formats or parses a date plus an hour/minute without an offset, such as '2011-12-03T01:10'. */ - private static final CompoundDateTimeFormatter STRICT_DATE_HOUR_MINUTE = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE_HOUR_MINUTE = new JavaDateFormatter("strict_date_hour_minute", DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm", Locale.ROOT)); /* * A strict date formatter that formats or parses a date without an offset, such as '2011-12-03'. */ - private static final CompoundDateTimeFormatter STRICT_YEAR_MONTH_DAY = new CompoundDateTimeFormatter(STRICT_YEAR_MONTH_DAY_FORMATTER); + private static final DateFormatter STRICT_YEAR_MONTH_DAY = + new JavaDateFormatter("strict_year_month_day", STRICT_YEAR_MONTH_DAY_FORMATTER); /* * A strict formatter that formats or parses a year and a month, such as '2011-12'. */ - private static final CompoundDateTimeFormatter STRICT_YEAR_MONTH = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder() + private static final DateFormatter STRICT_YEAR_MONTH = new JavaDateFormatter("strict_year_month", + new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) .appendLiteral("-") .appendValue(MONTH_OF_YEAR, 2, 2, SignStyle.NOT_NEGATIVE) @@ -395,15 +399,15 @@ public class DateFormatters { /* * A strict formatter that formats or parses a year, such as '2011'. */ - private static final CompoundDateTimeFormatter STRICT_YEAR = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder() + private static final DateFormatter STRICT_YEAR = new JavaDateFormatter("strict_year", new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) .toFormatter(Locale.ROOT)); /* * A strict formatter that formats or parses a hour, minute and second, such as '09:43:25'. */ - private static final CompoundDateTimeFormatter STRICT_HOUR_MINUTE_SECOND = - new CompoundDateTimeFormatter(STRICT_HOUR_MINUTE_SECOND_FORMATTER); + private static final DateFormatter STRICT_HOUR_MINUTE_SECOND = + new JavaDateFormatter("strict_hour_minute_second", STRICT_HOUR_MINUTE_SECOND_FORMATTER); private static final DateTimeFormatter STRICT_DATE_FORMATTER = new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) @@ -418,7 +422,8 @@ public class DateFormatters { * Returns a formatter that combines a full date and time, separated by a 'T' * (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter STRICT_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE_TIME = new JavaDateFormatter("strict_date_time", + new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_DATE_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -435,7 +440,7 @@ public class DateFormatters { * Returns a formatter for a full ordinal date and time without millis, * using a four digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter STRICT_ORDINAL_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_ORDINAL_DATE_TIME_NO_MILLIS = new JavaDateFormatter("strict_ordinal_date_time_no_millis", new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_NO_MILLIS_BASE) @@ -452,7 +457,9 @@ public class DateFormatters { * Returns a formatter that combines a full date and time without millis, * separated by a 'T' (yyyy-MM-dd'T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter STRICT_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE_TIME_NO_MILLIS = new JavaDateFormatter("strict_date_time_no_millis", + new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_DATE_TIME_NO_MILLIS_FORMATTER) @@ -478,17 +485,19 @@ public class DateFormatters { * NOTE: this is not a strict formatter to retain the joda time based behaviour, * even though it's named like this */ - private static final CompoundDateTimeFormatter STRICT_HOUR_MINUTE_SECOND_MILLIS = - new CompoundDateTimeFormatter(STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER, STRICT_HOUR_MINUTE_SECOND_MILLIS_FORMATTER); + private static final DateFormatter STRICT_HOUR_MINUTE_SECOND_MILLIS = + new JavaDateFormatter("strict_hour_minute_second_millis", + STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER, STRICT_HOUR_MINUTE_SECOND_MILLIS_FORMATTER); - private static final CompoundDateTimeFormatter STRICT_HOUR_MINUTE_SECOND_FRACTION = STRICT_HOUR_MINUTE_SECOND_MILLIS; + private static final DateFormatter STRICT_HOUR_MINUTE_SECOND_FRACTION = STRICT_HOUR_MINUTE_SECOND_MILLIS; /* * Returns a formatter that combines a full date, two digit hour of day, * two digit minute of hour, two digit second of minute, and three digit * fraction of second (yyyy-MM-dd'T'HH:mm:ss.SSS). */ - private static final CompoundDateTimeFormatter STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION = new JavaDateFormatter( + "strict_date_hour_minute_second_fraction", new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) .appendLiteral("T") @@ -503,20 +512,20 @@ public class DateFormatters { .toFormatter(Locale.ROOT) ); - private static final CompoundDateTimeFormatter STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS = STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION; + private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND_MILLIS = STRICT_DATE_HOUR_MINUTE_SECOND_FRACTION; /* * Returns a formatter for a two digit hour of day. (HH) */ - private static final CompoundDateTimeFormatter STRICT_HOUR = - new CompoundDateTimeFormatter(DateTimeFormatter.ofPattern("HH", Locale.ROOT)); + private static final DateFormatter STRICT_HOUR = + new JavaDateFormatter("strict_hour", DateTimeFormatter.ofPattern("HH", Locale.ROOT)); /* * Returns a formatter for a two digit hour of day and two digit minute of * hour. (HH:mm) */ - private static final CompoundDateTimeFormatter STRICT_HOUR_MINUTE = - new CompoundDateTimeFormatter(DateTimeFormatter.ofPattern("HH:mm", Locale.ROOT)); + private static final DateFormatter STRICT_HOUR_MINUTE = + new JavaDateFormatter("strict_hour_minute", DateTimeFormatter.ofPattern("HH:mm", Locale.ROOT)); private static final DateTimeFormatter STRICT_ORDINAL_DATE_TIME_FORMATTER_BASE = new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) @@ -535,7 +544,7 @@ public class DateFormatters { * Returns a formatter for a full ordinal date and time, using a four * digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter STRICT_ORDINAL_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_ORDINAL_DATE_TIME = new JavaDateFormatter("strict_ordinal_date_time", new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_FORMATTER_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_FORMATTER_BASE) @@ -566,7 +575,7 @@ public class DateFormatters { * hour, two digit second of minute, three digit fraction of second, and * time zone offset (HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter STRICT_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_TIME = new JavaDateFormatter("strict_time", new DateTimeFormatterBuilder().append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_TIME_FORMATTER_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_TIME_FORMATTER_BASE).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) @@ -577,7 +586,7 @@ public class DateFormatters { * hour, two digit second of minute, three digit fraction of second, and * time zone offset prefixed by 'T' ('T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter STRICT_T_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_T_TIME = new JavaDateFormatter("strict_t_time", new DateTimeFormatterBuilder().appendLiteral('T').append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendLiteral('T').append(STRICT_TIME_FORMATTER_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), @@ -597,7 +606,8 @@ public class DateFormatters { * Returns a formatter for a two digit hour of day, two digit minute of * hour, two digit second of minute, and time zone offset (HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter STRICT_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_TIME_NO_MILLIS = new JavaDateFormatter("strict_time_no_millis", + new DateTimeFormatterBuilder().append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(STRICT_TIME_NO_MILLIS_BASE).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -607,7 +617,9 @@ public class DateFormatters { * hour, two digit second of minute, and time zone offset prefixed * by 'T' ('T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter STRICT_T_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_T_TIME_NO_MILLIS = new JavaDateFormatter("strict_t_time_no_millis", + new DateTimeFormatterBuilder().appendLiteral("T").append(STRICT_TIME_NO_MILLIS_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendLiteral("T").append(STRICT_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendLiteral("T").append(STRICT_TIME_NO_MILLIS_BASE) @@ -632,13 +644,15 @@ public class DateFormatters { * Returns a formatter for a full date as four digit weekyear, two digit * week of weekyear, and one digit day of week (xxxx-'W'ww-e). */ - private static final CompoundDateTimeFormatter STRICT_WEEK_DATE = new CompoundDateTimeFormatter(ISO_WEEK_DATE); + private static final DateFormatter STRICT_WEEK_DATE = new JavaDateFormatter("strict_week_date", ISO_WEEK_DATE); /* * Returns a formatter that combines a full weekyear date and time without millis, * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter STRICT_WEEK_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter("strict_week_date_time_no_millis", + new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T) + .append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T) .append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T) @@ -649,7 +663,7 @@ public class DateFormatters { * Returns a formatter that combines a full weekyear date and time, * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter STRICT_WEEK_DATE_TIME = new CompoundDateTimeFormatter( + private static final DateFormatter STRICT_WEEK_DATE_TIME = new JavaDateFormatter("strict_week_date_time", new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T).append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T).append(STRICT_TIME_FORMATTER_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), @@ -660,7 +674,7 @@ public class DateFormatters { /* * Returns a formatter for a four digit weekyear */ - private static final CompoundDateTimeFormatter STRICT_WEEKYEAR = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder() + private static final DateFormatter STRICT_WEEKYEAR = new JavaDateFormatter("strict_weekyear", new DateTimeFormatterBuilder() .appendValue(WeekFields.ISO.weekBasedYear(), 4, 10, SignStyle.EXCEEDS_PAD) .toFormatter(Locale.ROOT)); @@ -674,13 +688,15 @@ public class DateFormatters { * Returns a formatter for a four digit weekyear and two digit week of * weekyear. (xxxx-'W'ww) */ - private static final CompoundDateTimeFormatter STRICT_WEEKYEAR_WEEK = new CompoundDateTimeFormatter(STRICT_WEEKYEAR_WEEK_FORMATTER); + private static final DateFormatter STRICT_WEEKYEAR_WEEK = + new JavaDateFormatter("strict_weekyear_week", STRICT_WEEKYEAR_WEEK_FORMATTER); /* * Returns a formatter for a four digit weekyear, two digit week of * weekyear, and one digit day of week. (xxxx-'W'ww-e) */ - private static final CompoundDateTimeFormatter STRICT_WEEKYEAR_WEEK_DAY = new CompoundDateTimeFormatter(new DateTimeFormatterBuilder() + private static final DateFormatter STRICT_WEEKYEAR_WEEK_DAY = new JavaDateFormatter("strict_weekyear_week_day", + new DateTimeFormatterBuilder() .append(STRICT_WEEKYEAR_WEEK_FORMATTER) .appendLiteral("-") .appendValue(WeekFields.ISO.dayOfWeek()) @@ -691,14 +707,14 @@ public class DateFormatters { * two digit minute of hour, and two digit second of * minute. (yyyy-MM-dd'T'HH:mm:ss) */ - private static final CompoundDateTimeFormatter STRICT_DATE_HOUR_MINUTE_SECOND = - new CompoundDateTimeFormatter(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT)); + private static final DateFormatter STRICT_DATE_HOUR_MINUTE_SECOND = new JavaDateFormatter("strict_date_hour_minute_second", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT)); /* * A basic formatter for a full date as four digit year, two digit * month of year, and two digit day of month (yyyyMMdd). */ - private static final CompoundDateTimeFormatter BASIC_DATE = new CompoundDateTimeFormatter( + private static final DateFormatter BASIC_DATE = new JavaDateFormatter("basic_date", new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR, 4, 4, SignStyle.NORMAL) .appendValue(MONTH_OF_YEAR, 2, 2, SignStyle.NOT_NEGATIVE) @@ -723,7 +739,7 @@ public class DateFormatters { * Returns a formatter for a full ordinal date, using a four * digit year and three digit dayOfYear (yyyy-DDD). */ - private static final CompoundDateTimeFormatter STRICT_ORDINAL_DATE = new CompoundDateTimeFormatter(STRICT_ORDINAL_DATE_FORMATTER); + private static final DateFormatter STRICT_ORDINAL_DATE = new JavaDateFormatter("strict_ordinal_date", STRICT_ORDINAL_DATE_FORMATTER); ///////////////////////////////////////// // @@ -759,7 +775,8 @@ public class DateFormatters { * a date formatter with optional time, being very lenient, format is * yyyy-MM-dd'T'HH:mm:ss.SSSZ */ - private static final CompoundDateTimeFormatter DATE_OPTIONAL_TIME = new CompoundDateTimeFormatter(STRICT_DATE_OPTIONAL_TIME.printer, + private static final DateFormatter DATE_OPTIONAL_TIME = new JavaDateFormatter("date_optional_time", + STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, new DateTimeFormatterBuilder() .append(DATE_FORMATTER) .optionalStart() @@ -834,8 +851,8 @@ public class DateFormatters { * Returns a formatter for a full ordinal date, using a four * digit year and three digit dayOfYear (yyyy-DDD). */ - private static final CompoundDateTimeFormatter ORDINAL_DATE = - new CompoundDateTimeFormatter(ORDINAL_DATE_PRINTER, ORDINAL_DATE_FORMATTER); + private static final DateFormatter ORDINAL_DATE = + new JavaDateFormatter("ordinal_date", ORDINAL_DATE_PRINTER, ORDINAL_DATE_FORMATTER); private static final DateTimeFormatter TIME_NO_MILLIS_FORMATTER = new DateTimeFormatterBuilder() .appendValue(HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE) @@ -864,70 +881,32 @@ public class DateFormatters { /* * Returns a formatter for a four digit weekyear. (YYYY) */ - private static final CompoundDateTimeFormatter WEEK_YEAR = new CompoundDateTimeFormatter( + private static final DateFormatter WEEK_YEAR = new JavaDateFormatter("week_year", new DateTimeFormatterBuilder().appendValue(WeekFields.ISO.weekBasedYear()).toFormatter(Locale.ROOT)); /* * Returns a formatter for a four digit weekyear. (uuuu) */ - private static final CompoundDateTimeFormatter YEAR = new CompoundDateTimeFormatter( + private static final DateFormatter YEAR = new JavaDateFormatter("year", new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR).toFormatter(Locale.ROOT)); /* * Returns a formatter for parsing the seconds since the epoch */ - private static final CompoundDateTimeFormatter EPOCH_SECOND = new CompoundDateTimeFormatter( + private static final DateFormatter EPOCH_SECOND = new JavaDateFormatter("epoch_second", new DateTimeFormatterBuilder().appendValue(ChronoField.INSTANT_SECONDS).toFormatter(Locale.ROOT)); /* - * Returns a formatter for parsing the milliseconds since the epoch - * This one needs a custom implementation, because the standard date formatter can not parse negative values - * or anything +- 999 milliseconds around the epoch - * - * This implementation just resorts to parsing the input directly to an Instant by trying to parse a number. + * Parses the milliseconds since/before the epoch */ - private static final DateTimeFormatter EPOCH_MILLIS_FORMATTER = new DateTimeFormatterBuilder() - .appendValue(ChronoField.INSTANT_SECONDS, 1, 19, SignStyle.NEVER) - .appendValue(ChronoField.MILLI_OF_SECOND, 3) - .toFormatter(Locale.ROOT); - - private static final class EpochDateTimeFormatter extends CompoundDateTimeFormatter { - - private EpochDateTimeFormatter() { - super(EPOCH_MILLIS_FORMATTER); - } - - private EpochDateTimeFormatter(ZoneId zoneId) { - super(EPOCH_MILLIS_FORMATTER.withZone(zoneId)); - } - - @Override - public TemporalAccessor parse(String input) { - try { - return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC); - } catch (NumberFormatException e) { - throw new DateTimeParseException("invalid number", input, 0, e); - } - } - - @Override - public CompoundDateTimeFormatter withZone(ZoneId zoneId) { - return new EpochDateTimeFormatter(zoneId); - } - - @Override - public String format(TemporalAccessor accessor) { - return String.valueOf(Instant.from(accessor).toEpochMilli()); - } - } - - private static final CompoundDateTimeFormatter EPOCH_MILLIS = new EpochDateTimeFormatter(); + private static final DateFormatter EPOCH_MILLIS = EpochMillisDateFormatter.INSTANCE; /* * Returns a formatter that combines a full date and two digit hour of * day. (yyyy-MM-dd'T'HH) */ - private static final CompoundDateTimeFormatter DATE_HOUR = new CompoundDateTimeFormatter(STRICT_DATE_HOUR.printer, + private static final DateFormatter DATE_HOUR = new JavaDateFormatter("date_hour", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH", Locale.ROOT), new DateTimeFormatterBuilder() .append(DATE_FORMATTER) .appendLiteral("T") @@ -940,8 +919,8 @@ public class DateFormatters { * fraction of second (yyyy-MM-dd'T'HH:mm:ss.SSS). Parsing will parse up * to 3 fractional second digits. */ - private static final CompoundDateTimeFormatter DATE_HOUR_MINUTE_SECOND_MILLIS = - new CompoundDateTimeFormatter( + private static final DateFormatter DATE_HOUR_MINUTE_SECOND_MILLIS = + new JavaDateFormatter("date_hour_minute_second_millis", new DateTimeFormatterBuilder() .append(STRICT_YEAR_MONTH_DAY_FORMATTER) .appendLiteral("T") @@ -953,13 +932,14 @@ public class DateFormatters { .append(HOUR_MINUTE_SECOND_MILLIS_FORMATTER) .toFormatter(Locale.ROOT)); - private static final CompoundDateTimeFormatter DATE_HOUR_MINUTE_SECOND_FRACTION = DATE_HOUR_MINUTE_SECOND_MILLIS; + private static final DateFormatter DATE_HOUR_MINUTE_SECOND_FRACTION = DATE_HOUR_MINUTE_SECOND_MILLIS; /* * Returns a formatter that combines a full date, two digit hour of day, * and two digit minute of hour. (yyyy-MM-dd'T'HH:mm) */ - private static final CompoundDateTimeFormatter DATE_HOUR_MINUTE = new CompoundDateTimeFormatter(STRICT_DATE_HOUR_MINUTE.printer, + private static final DateFormatter DATE_HOUR_MINUTE = new JavaDateFormatter("date_hour_minute", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm", Locale.ROOT), new DateTimeFormatterBuilder() .append(DATE_FORMATTER) .appendLiteral("T") @@ -971,8 +951,8 @@ public class DateFormatters { * two digit minute of hour, and two digit second of * minute. (yyyy-MM-dd'T'HH:mm:ss) */ - private static final CompoundDateTimeFormatter DATE_HOUR_MINUTE_SECOND = new CompoundDateTimeFormatter( - STRICT_DATE_HOUR_MINUTE_SECOND.printer, + private static final DateFormatter DATE_HOUR_MINUTE_SECOND = new JavaDateFormatter("date_hour_minute_second", + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss", Locale.ROOT), new DateTimeFormatterBuilder() .append(DATE_FORMATTER) .appendLiteral("T") @@ -994,8 +974,8 @@ public class DateFormatters { * Returns a formatter that combines a full date and time, separated by a 'T' * (yyyy-MM-dd'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter DATE_TIME = new CompoundDateTimeFormatter( - STRICT_DATE_TIME.printer, + private static final DateFormatter DATE_TIME = new JavaDateFormatter("date_time", + STRICT_DATE_OPTIONAL_TIME_FORMATTER_1, new DateTimeFormatterBuilder().append(DATE_TIME_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(DATE_TIME_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -1004,20 +984,22 @@ public class DateFormatters { * Returns a basic formatter for a full date as four digit weekyear, two * digit week of weekyear, and one digit day of week (YYYY'W'wwe). */ - private static final CompoundDateTimeFormatter BASIC_WEEK_DATE = - new CompoundDateTimeFormatter(STRICT_BASIC_WEEK_DATE.printer, BASIC_WEEK_DATE_FORMATTER); + private static final DateFormatter BASIC_WEEK_DATE = + new JavaDateFormatter("basic_week_date", STRICT_BASIC_WEEK_DATE_PRINTER, BASIC_WEEK_DATE_FORMATTER); /* * Returns a formatter for a full date as four digit year, two digit month * of year, and two digit day of month (yyyy-MM-dd). */ - private static final CompoundDateTimeFormatter DATE = new CompoundDateTimeFormatter(STRICT_DATE.printer, DATE_FORMATTER); + private static final DateFormatter DATE = new JavaDateFormatter("date", + DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.LENIENT), + DATE_FORMATTER); // only the formatter, nothing optional here private static final DateTimeFormatter DATE_TIME_NO_MILLIS_PRINTER = new DateTimeFormatterBuilder() - .append(STRICT_DATE.printer) + .append(DateTimeFormatter.ISO_LOCAL_DATE.withResolverStyle(ResolverStyle.LENIENT)) .appendLiteral('T') - .append(STRICT_HOUR_MINUTE.printer) + .appendPattern("HH:mm") .appendLiteral(':') .appendValue(SECOND_OF_MINUTE, 2, 2, SignStyle.NOT_NEGATIVE) .appendZoneId() @@ -1037,7 +1019,8 @@ public class DateFormatters { * Returns a formatter that combines a full date and time without millis, but with a timezone that can be optional * separated by a 'T' (yyyy-MM-dd'T'HH:mm:ssZ). */ - private static final CompoundDateTimeFormatter DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter(DATE_TIME_NO_MILLIS_PRINTER, + private static final DateFormatter DATE_TIME_NO_MILLIS = new JavaDateFormatter("date_time_no_millis", + DATE_TIME_NO_MILLIS_PRINTER, new DateTimeFormatterBuilder().append(DATE_TIME_PREFIX).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(DATE_TIME_PREFIX).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(DATE_TIME_PREFIX) @@ -1051,21 +1034,21 @@ public class DateFormatters { * hour, two digit second of minute, and three digit fraction of * second (HH:mm:ss.SSS). */ - private static final CompoundDateTimeFormatter HOUR_MINUTE_SECOND_MILLIS = - new CompoundDateTimeFormatter(STRICT_HOUR_MINUTE_SECOND_FRACTION.printer, HOUR_MINUTE_SECOND_MILLIS_FORMATTER); + private static final DateFormatter HOUR_MINUTE_SECOND_MILLIS = new JavaDateFormatter("hour_minute_second_millis", + STRICT_HOUR_MINUTE_SECOND_MILLIS_PRINTER, HOUR_MINUTE_SECOND_MILLIS_FORMATTER); /* * Returns a formatter for a two digit hour of day and two digit minute of * hour. (HH:mm) */ - private static final CompoundDateTimeFormatter HOUR_MINUTE = - new CompoundDateTimeFormatter(STRICT_HOUR_MINUTE.printer, HOUR_MINUTE_FORMATTER); + private static final DateFormatter HOUR_MINUTE = + new JavaDateFormatter("hour_minute", DateTimeFormatter.ofPattern("HH:mm", Locale.ROOT), HOUR_MINUTE_FORMATTER); /* * A strict formatter that formats or parses a hour, minute and second, such as '09:43:25'. */ - private static final CompoundDateTimeFormatter HOUR_MINUTE_SECOND = new CompoundDateTimeFormatter( - STRICT_HOUR_MINUTE_SECOND.printer, + private static final DateFormatter HOUR_MINUTE_SECOND = new JavaDateFormatter("hour_minute_second", + STRICT_HOUR_MINUTE_SECOND_FORMATTER, new DateTimeFormatterBuilder() .append(HOUR_MINUTE_FORMATTER) .appendLiteral(":") @@ -1076,8 +1059,8 @@ public class DateFormatters { /* * Returns a formatter for a two digit hour of day. (HH) */ - private static final CompoundDateTimeFormatter HOUR = new CompoundDateTimeFormatter( - STRICT_HOUR.printer, + private static final DateFormatter HOUR = new JavaDateFormatter("hour", + DateTimeFormatter.ofPattern("HH", Locale.ROOT), new DateTimeFormatterBuilder().appendValue(HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE).toFormatter(Locale.ROOT) ); @@ -1096,8 +1079,9 @@ public class DateFormatters { * Returns a formatter for a full ordinal date and time, using a four * digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter ORDINAL_DATE_TIME = new CompoundDateTimeFormatter( - STRICT_ORDINAL_DATE_TIME.printer, + private static final DateFormatter ORDINAL_DATE_TIME = new JavaDateFormatter("ordinal_date_time", + new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_FORMATTER_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ORDINAL_DATE_TIME_FORMATTER_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ORDINAL_DATE_TIME_FORMATTER_BASE) @@ -1114,8 +1098,9 @@ public class DateFormatters { * Returns a formatter for a full ordinal date and time without millis, * using a four digit year and three digit dayOfYear (yyyy-DDD'T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter ORDINAL_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( - STRICT_ORDINAL_DATE_TIME_NO_MILLIS.printer, + private static final DateFormatter ORDINAL_DATE_TIME_NO_MILLIS = new JavaDateFormatter("ordinal_date_time_no_millis", + new DateTimeFormatterBuilder().append(STRICT_ORDINAL_DATE_TIME_NO_MILLIS_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ORDINAL_DATE_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(ORDINAL_DATE_TIME_NO_MILLIS_BASE) @@ -1126,8 +1111,8 @@ public class DateFormatters { * Returns a formatter that combines a full weekyear date and time, * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter WEEK_DATE_TIME = new CompoundDateTimeFormatter( - STRICT_WEEK_DATE_TIME.printer, + private static final DateFormatter WEEK_DATE_TIME = new JavaDateFormatter("week_date_time", + new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T).append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(WEEK_DATE_FORMATTER).appendLiteral("T").append(TIME_PREFIX) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(WEEK_DATE_FORMATTER).appendLiteral("T").append(TIME_PREFIX) @@ -1138,8 +1123,9 @@ public class DateFormatters { * Returns a formatter that combines a full weekyear date and time, * separated by a 'T' (xxxx-'W'ww-e'T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter WEEK_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( - STRICT_WEEK_DATE_TIME_NO_MILLIS.printer, + private static final DateFormatter WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter("week_date_time_no_millis", + new DateTimeFormatterBuilder().append(ISO_WEEK_DATE_T) + .append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(WEEK_DATE_FORMATTER).append(T_TIME_NO_MILLIS_FORMATTER) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(WEEK_DATE_FORMATTER).append(T_TIME_NO_MILLIS_FORMATTER) @@ -1150,8 +1136,11 @@ public class DateFormatters { * Returns a basic formatter that combines a basic weekyear date and time, * separated by a 'T' (xxxx'W'wwe'T'HHmmss.SSSX). */ - private static final CompoundDateTimeFormatter BASIC_WEEK_DATE_TIME = new CompoundDateTimeFormatter( - STRICT_BASIC_WEEK_DATE_TIME.printer, + private static final DateFormatter BASIC_WEEK_DATE_TIME = new JavaDateFormatter("basic_week_date_time", + new DateTimeFormatterBuilder() + .append(STRICT_BASIC_WEEK_DATE_PRINTER) + .append(DateTimeFormatter.ofPattern("'T'HHmmss.SSSX", Locale.ROOT)) + .toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_WEEK_DATE_FORMATTER).append(BASIC_T_TIME_FORMATTER) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_WEEK_DATE_FORMATTER).append(BASIC_T_TIME_FORMATTER) @@ -1162,8 +1151,10 @@ public class DateFormatters { * Returns a basic formatter that combines a basic weekyear date and time, * separated by a 'T' (xxxx'W'wwe'T'HHmmssX). */ - private static final CompoundDateTimeFormatter BASIC_WEEK_DATE_TIME_NO_MILLIS = new CompoundDateTimeFormatter( - STRICT_BASIC_WEEK_DATE_TIME_NO_MILLIS.printer, + private static final DateFormatter BASIC_WEEK_DATE_TIME_NO_MILLIS = new JavaDateFormatter("basic_week_date_time_no_millis", + new DateTimeFormatterBuilder() + .append(STRICT_BASIC_WEEK_DATE_PRINTER).append(DateTimeFormatter.ofPattern("'T'HHmmssX", Locale.ROOT)) + .toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_WEEK_DATE_FORMATTER).appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(BASIC_WEEK_DATE_FORMATTER).appendLiteral("T").append(BASIC_TIME_NO_MILLIS_BASE) @@ -1175,8 +1166,8 @@ public class DateFormatters { * hour, two digit second of minute, three digit fraction of second, and * time zone offset (HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter TIME = new CompoundDateTimeFormatter( - STRICT_TIME.printer, + private static final DateFormatter TIME = new JavaDateFormatter("time", + new DateTimeFormatterBuilder().append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(TIME_PREFIX).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(TIME_PREFIX).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -1185,8 +1176,8 @@ public class DateFormatters { * Returns a formatter for a two digit hour of day, two digit minute of * hour, two digit second of minute, andtime zone offset (HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter TIME_NO_MILLIS = new CompoundDateTimeFormatter( - STRICT_TIME_NO_MILLIS.printer, + private static final DateFormatter TIME_NO_MILLIS = new JavaDateFormatter("time_no_millis", + new DateTimeFormatterBuilder().append(STRICT_TIME_NO_MILLIS_BASE).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(TIME_NO_MILLIS_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(TIME_NO_MILLIS_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -1196,8 +1187,8 @@ public class DateFormatters { * hour, two digit second of minute, three digit fraction of second, and * time zone offset prefixed by 'T' ('T'HH:mm:ss.SSSZZ). */ - private static final CompoundDateTimeFormatter T_TIME = new CompoundDateTimeFormatter( - STRICT_T_TIME.printer, + private static final DateFormatter T_TIME = new JavaDateFormatter("t_time", + new DateTimeFormatterBuilder().appendLiteral('T').append(STRICT_TIME_PRINTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendLiteral("T").append(TIME_PREFIX) .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendLiteral("T").append(TIME_PREFIX) @@ -1209,8 +1200,9 @@ public class DateFormatters { * hour, two digit second of minute, and time zone offset prefixed * by 'T' ('T'HH:mm:ssZZ). */ - private static final CompoundDateTimeFormatter T_TIME_NO_MILLIS = new CompoundDateTimeFormatter( - STRICT_T_TIME_NO_MILLIS.printer, + private static final DateFormatter T_TIME_NO_MILLIS = new JavaDateFormatter("t_time_no_millis", + new DateTimeFormatterBuilder().appendLiteral("T").append(STRICT_TIME_NO_MILLIS_BASE) + .appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(T_TIME_NO_MILLIS_FORMATTER).appendZoneOrOffsetId().toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().append(T_TIME_NO_MILLIS_FORMATTER).append(TIME_ZONE_FORMATTER_NO_COLON).toFormatter(Locale.ROOT) ); @@ -1218,16 +1210,20 @@ public class DateFormatters { /* * A strict formatter that formats or parses a year and a month, such as '2011-12'. */ - private static final CompoundDateTimeFormatter YEAR_MONTH = new CompoundDateTimeFormatter( - STRICT_YEAR_MONTH.printer, + private static final DateFormatter YEAR_MONTH = new JavaDateFormatter("year_month", + new DateTimeFormatterBuilder() + .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD) + .appendLiteral("-") + .appendValue(MONTH_OF_YEAR, 2, 2, SignStyle.NOT_NEGATIVE) + .toFormatter(Locale.ROOT), new DateTimeFormatterBuilder().appendValue(ChronoField.YEAR).appendLiteral("-").appendValue(MONTH_OF_YEAR).toFormatter(Locale.ROOT) ); /* * A strict date formatter that formats or parses a date without an offset, such as '2011-12-03'. */ - private static final CompoundDateTimeFormatter YEAR_MONTH_DAY = new CompoundDateTimeFormatter( - STRICT_YEAR_MONTH_DAY.printer, + private static final DateFormatter YEAR_MONTH_DAY = new JavaDateFormatter("year_month_day", + STRICT_YEAR_MONTH_DAY_FORMATTER, new DateTimeFormatterBuilder() .appendValue(ChronoField.YEAR) .appendLiteral("-") @@ -1241,13 +1237,13 @@ public class DateFormatters { * Returns a formatter for a full date as four digit weekyear, two digit * week of weekyear, and one digit day of week (xxxx-'W'ww-e). */ - private static final CompoundDateTimeFormatter WEEK_DATE = new CompoundDateTimeFormatter(STRICT_WEEK_DATE.printer, WEEK_DATE_FORMATTER); + private static final DateFormatter WEEK_DATE = new JavaDateFormatter("week_date", ISO_WEEK_DATE, WEEK_DATE_FORMATTER); /* * Returns a formatter for a four digit weekyear and two digit week of * weekyear. (xxxx-'W'ww) */ - private static final CompoundDateTimeFormatter WEEKYEAR_WEEK = new CompoundDateTimeFormatter(STRICT_WEEKYEAR_WEEK.printer, + private static final DateFormatter WEEKYEAR_WEEK = new JavaDateFormatter("weekyear_week", STRICT_WEEKYEAR_WEEK_FORMATTER, new DateTimeFormatterBuilder() .appendValue(WeekFields.ISO.weekBasedYear()) .appendLiteral("-W") @@ -1259,8 +1255,12 @@ public class DateFormatters { * Returns a formatter for a four digit weekyear, two digit week of * weekyear, and one digit day of week. (xxxx-'W'ww-e) */ - private static final CompoundDateTimeFormatter WEEKYEAR_WEEK_DAY = new CompoundDateTimeFormatter( - STRICT_WEEKYEAR_WEEK_DAY.printer, + private static final DateFormatter WEEKYEAR_WEEK_DAY = new JavaDateFormatter("weekyear_week_day", + new DateTimeFormatterBuilder() + .append(STRICT_WEEKYEAR_WEEK_FORMATTER) + .appendLiteral("-") + .appendValue(WeekFields.ISO.dayOfWeek()) + .toFormatter(Locale.ROOT), new DateTimeFormatterBuilder() .appendValue(WeekFields.ISO.weekBasedYear()) .appendLiteral("-W") @@ -1276,11 +1276,11 @@ public class DateFormatters { // ///////////////////////////////////////// - public static CompoundDateTimeFormatter forPattern(String input) { + public static DateFormatter forPattern(String input) { return forPattern(input, Locale.ROOT); } - public static CompoundDateTimeFormatter forPattern(String input, Locale locale) { + public static DateFormatter forPattern(String input, Locale locale) { if (Strings.hasLength(input)) { input = input.trim(); } @@ -1452,21 +1452,20 @@ public class DateFormatters { if (formats.length == 1) { return forPattern(formats[0], locale); } else { - Collection parsers = new LinkedHashSet<>(formats.length); - for (String format : formats) { - CompoundDateTimeFormatter dateTimeFormatter = forPattern(format, locale); - try { - parsers.addAll(Arrays.asList(dateTimeFormatter.parsers)); - } catch (IllegalArgumentException e) { - throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e); + try { + DateFormatter[] formatters = new DateFormatter[formats.length]; + for (int i = 0; i < formats.length; i++) { + formatters[i] = forPattern(formats[i], locale); } - } - return new CompoundDateTimeFormatter(parsers.toArray(new DateTimeFormatter[0])); + return DateFormatter.merge(formatters); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e); + } } } else { try { - return new CompoundDateTimeFormatter(new DateTimeFormatterBuilder().appendPattern(input).toFormatter(locale)); + return new JavaDateFormatter(input, new DateTimeFormatterBuilder().appendPattern(input).toFormatter(locale)); } catch (IllegalArgumentException e) { throw new IllegalArgumentException("Invalid format: [" + input + "]: " + e.getMessage(), e); } diff --git a/server/src/main/java/org/elasticsearch/common/time/DateMathParser.java b/server/src/main/java/org/elasticsearch/common/time/DateMathParser.java index 39f6dabbdb2..5e5ecc5bafd 100644 --- a/server/src/main/java/org/elasticsearch/common/time/DateMathParser.java +++ b/server/src/main/java/org/elasticsearch/common/time/DateMathParser.java @@ -58,10 +58,10 @@ public class DateMathParser { ROUND_UP_BASE_FIELDS.put(ChronoField.MILLI_OF_SECOND, 999L); } - private final CompoundDateTimeFormatter formatter; - private final CompoundDateTimeFormatter roundUpFormatter; + private final DateFormatter formatter; + private final DateFormatter roundUpFormatter; - public DateMathParser(CompoundDateTimeFormatter formatter) { + public DateMathParser(DateFormatter formatter) { Objects.requireNonNull(formatter); this.formatter = formatter; this.roundUpFormatter = formatter.parseDefaulting(ROUND_UP_BASE_FIELDS); @@ -247,7 +247,7 @@ public class DateMathParser { } private long parseDateTime(String value, ZoneId timeZone, boolean roundUpIfNoTime) { - CompoundDateTimeFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter; + DateFormatter formatter = roundUpIfNoTime ? this.roundUpFormatter : this.formatter; try { if (timeZone == null) { return DateFormatters.toZonedDateTime(formatter.parse(value)).toInstant().toEpochMilli(); diff --git a/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java new file mode 100644 index 00000000000..d50cc0cf466 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/common/time/EpochMillisDateFormatter.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.common.time; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeParseException; +import java.time.temporal.TemporalAccessor; +import java.time.temporal.TemporalField; +import java.util.Map; + +/** + * This is a special formatter to parse the milliseconds since the epoch. + * There is no way using a native java time date formatter to resemble + * the required behaviour to parse negative milliseconds as well. + * + * This implementation simply tries to convert the input to a long and uses + * this as the milliseconds since the epoch without involving any other + * java time code + */ +class EpochMillisDateFormatter implements DateFormatter { + + public static DateFormatter INSTANCE = new EpochMillisDateFormatter(); + + private EpochMillisDateFormatter() {} + + @Override + public TemporalAccessor parse(String input) { + try { + return Instant.ofEpochMilli(Long.valueOf(input)).atZone(ZoneOffset.UTC); + } catch (NumberFormatException e) { + throw new DateTimeParseException("invalid number", input, 0, e); + } + } + + @Override + public DateFormatter withZone(ZoneId zoneId) { + return this; + } + + @Override + public String format(TemporalAccessor accessor) { + return String.valueOf(Instant.from(accessor).toEpochMilli()); + } + + @Override + public String pattern() { + return "epoch_millis"; + } + + @Override + public DateFormatter parseDefaulting(Map fields) { + return this; + } +} diff --git a/server/src/main/java/org/elasticsearch/common/time/CompoundDateTimeFormatter.java b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java similarity index 59% rename from server/src/main/java/org/elasticsearch/common/time/CompoundDateTimeFormatter.java rename to server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java index 0332c03814d..f68215fde49 100644 --- a/server/src/main/java/org/elasticsearch/common/time/CompoundDateTimeFormatter.java +++ b/server/src/main/java/org/elasticsearch/common/time/JavaDateFormatter.java @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ + package org.elasticsearch.common.time; import java.time.ZoneId; @@ -27,33 +28,28 @@ import java.time.temporal.TemporalField; import java.util.Arrays; import java.util.Locale; import java.util.Map; -import java.util.function.Consumer; -/** - * wrapper class around java.time.DateTimeFormatter that supports multiple formats for easier parsing, - * and one specific format for printing - */ -public class CompoundDateTimeFormatter { +class JavaDateFormatter implements DateFormatter { - private static final Consumer SAME_TIME_ZONE_VALIDATOR = (parsers) -> { + private final String format; + private final DateTimeFormatter printer; + private final DateTimeFormatter[] parsers; + + JavaDateFormatter(String format, DateTimeFormatter printer, DateTimeFormatter... parsers) { long distinctZones = Arrays.stream(parsers).map(DateTimeFormatter::getZone).distinct().count(); if (distinctZones > 1) { throw new IllegalArgumentException("formatters must have the same time zone"); } - }; - - final DateTimeFormatter printer; - final DateTimeFormatter[] parsers; - - CompoundDateTimeFormatter(DateTimeFormatter ... parsers) { if (parsers.length == 0) { - throw new IllegalArgumentException("at least one date time formatter is required"); + this.parsers = new DateTimeFormatter[]{printer}; + } else { + this.parsers = parsers; } - SAME_TIME_ZONE_VALIDATOR.accept(parsers); - this.printer = parsers[0]; - this.parsers = parsers; + this.format = format; + this.printer = printer; } + @Override public TemporalAccessor parse(String input) { DateTimeParseException failure = null; for (int i = 0; i < parsers.length; i++) { @@ -72,13 +68,8 @@ public class CompoundDateTimeFormatter { throw failure; } - /** - * Configure a specific time zone for a date formatter - * - * @param zoneId The zoneId this formatter shoulduse - * @return The new formatter with all parsers switched to the specified timezone - */ - public CompoundDateTimeFormatter withZone(ZoneId zoneId) { + @Override + public DateFormatter withZone(ZoneId zoneId) { // shortcurt to not create new objects unnecessarily if (zoneId.equals(parsers[0].getZone())) { return this; @@ -89,25 +80,33 @@ public class CompoundDateTimeFormatter { parsersWithZone[i] = parsers[i].withZone(zoneId); } - return new CompoundDateTimeFormatter(parsersWithZone); - } - - /** - * Configure defaults for missing values in a parser, then return a new compound date formatter - */ - CompoundDateTimeFormatter parseDefaulting(Map fields) { - final DateTimeFormatter[] parsersWithDefaulting = new DateTimeFormatter[parsers.length]; - for (int i = 0; i < parsers.length; i++) { - DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(parsers[i]); - fields.forEach(builder::parseDefaulting); - parsersWithDefaulting[i] = builder.toFormatter(Locale.ROOT); - } - - return new CompoundDateTimeFormatter(parsersWithDefaulting); + return new JavaDateFormatter(format, printer.withZone(zoneId), parsersWithZone); } + @Override public String format(TemporalAccessor accessor) { return printer.format(accessor); } + @Override + public String pattern() { + return format; + } + + @Override + public DateFormatter parseDefaulting(Map fields) { + final DateTimeFormatterBuilder parseDefaultingBuilder = new DateTimeFormatterBuilder().append(printer); + fields.forEach(parseDefaultingBuilder::parseDefaulting); + if (parsers.length == 1 && parsers[0].equals(printer)) { + return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT)); + } else { + final DateTimeFormatter[] parsersWithDefaulting = new DateTimeFormatter[parsers.length]; + for (int i = 0; i < parsers.length; i++) { + DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder().append(parsers[i]); + fields.forEach(builder::parseDefaulting); + parsersWithDefaulting[i] = builder.toFormatter(Locale.ROOT); + } + return new JavaDateFormatter(format, parseDefaultingBuilder.toFormatter(Locale.ROOT), parsersWithDefaulting); + } + } } diff --git a/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java b/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java index 5793bcf8a0e..684e96f678c 100644 --- a/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java +++ b/server/src/main/java/org/elasticsearch/common/xcontent/XContentElasticsearchExtension.java @@ -21,7 +21,7 @@ package org.elasticsearch.common.xcontent; import org.apache.lucene.util.BytesRef; import org.elasticsearch.common.bytes.BytesReference; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.ByteSizeValue; import org.elasticsearch.common.unit.TimeValue; @@ -64,9 +64,9 @@ import java.util.function.Function; public class XContentElasticsearchExtension implements XContentBuilderExtension { public static final DateTimeFormatter DEFAULT_DATE_PRINTER = ISODateTimeFormat.dateTime().withZone(DateTimeZone.UTC); - public static final CompoundDateTimeFormatter DEFAULT_FORMATTER = DateFormatters.forPattern("strict_date_optional_time_nanos"); - public static final CompoundDateTimeFormatter LOCAL_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSS"); - public static final CompoundDateTimeFormatter OFFSET_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSSZZZZZ"); + public static final DateFormatter DEFAULT_FORMATTER = DateFormatters.forPattern("strict_date_optional_time_nanos"); + public static final DateFormatter LOCAL_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSS"); + public static final DateFormatter OFFSET_TIME_FORMATTER = DateFormatters.forPattern("HH:mm:ss.SSSZZZZZ"); @Override public Map, XContentBuilder.Writer> getXContentWriters() { diff --git a/server/src/main/java/org/elasticsearch/monitor/jvm/HotThreads.java b/server/src/main/java/org/elasticsearch/monitor/jvm/HotThreads.java index 10a3e81163a..7e00aaa7cd9 100644 --- a/server/src/main/java/org/elasticsearch/monitor/jvm/HotThreads.java +++ b/server/src/main/java/org/elasticsearch/monitor/jvm/HotThreads.java @@ -21,7 +21,7 @@ package org.elasticsearch.monitor.jvm; import org.apache.lucene.util.CollectionUtil; import org.elasticsearch.ElasticsearchException; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.TimeValue; @@ -43,7 +43,7 @@ public class HotThreads { private static final Object mutex = new Object(); - private static final CompoundDateTimeFormatter DATE_TIME_FORMATTER = DateFormatters.forPattern("dateOptionalTime"); + private static final DateFormatter DATE_TIME_FORMATTER = DateFormatters.forPattern("dateOptionalTime"); private int busiestThreads = 3; private TimeValue interval = new TimeValue(500, TimeUnit.MILLISECONDS); diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java index 2da5e432ca3..fb302b1b3b3 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestSnapshotAction.java @@ -25,7 +25,7 @@ import org.elasticsearch.action.admin.cluster.snapshots.get.GetSnapshotsResponse import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Table; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.rest.RestController; @@ -99,7 +99,7 @@ public class RestSnapshotAction extends AbstractCatAction { .endHeaders(); } - private static final CompoundDateTimeFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); + private static final DateFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); private Table buildTable(RestRequest req, GetSnapshotsResponse getSnapshotsResponse) { Table table = getTableWithHeader(req); diff --git a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTasksAction.java b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTasksAction.java index 7d14422b37c..39b3f08dcdc 100644 --- a/server/src/main/java/org/elasticsearch/rest/action/cat/RestTasksAction.java +++ b/server/src/main/java/org/elasticsearch/rest/action/cat/RestTasksAction.java @@ -27,7 +27,7 @@ import org.elasticsearch.cluster.node.DiscoveryNodes; import org.elasticsearch.common.Strings; import org.elasticsearch.common.Table; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.rest.RestController; @@ -125,7 +125,7 @@ public class RestTasksAction extends AbstractCatAction { return table; } - private static final CompoundDateTimeFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); + private static final DateFormatter FORMATTER = DateFormatters.forPattern("HH:mm:ss").withZone(ZoneOffset.UTC); private void buildRow(Table table, boolean fullId, boolean detailed, DiscoveryNodes discoveryNodes, TaskInfo taskInfo) { table.startRow(); diff --git a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java index fdbe74d8d4d..38e31519ea1 100644 --- a/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java +++ b/server/src/main/java/org/elasticsearch/snapshots/SnapshotInfo.java @@ -27,7 +27,7 @@ import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.ObjectParser; @@ -52,7 +52,7 @@ public final class SnapshotInfo implements Comparable, ToXContent, public static final String CONTEXT_MODE_PARAM = "context_mode"; public static final String CONTEXT_MODE_SNAPSHOT = "SNAPSHOT"; - private static final CompoundDateTimeFormatter DATE_TIME_FORMATTER = DateFormatters.forPattern("strictDateOptionalTime"); + private static final DateFormatter DATE_TIME_FORMATTER = DateFormatters.forPattern("strictDateOptionalTime"); private static final String SNAPSHOT = "snapshot"; private static final String UUID = "uuid"; private static final String INDICES = "indices"; diff --git a/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java b/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java index cea83bfbccf..5203aa07d28 100644 --- a/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java +++ b/server/src/test/java/org/elasticsearch/common/joda/JavaJodaTimeDuellingTests.java @@ -19,7 +19,7 @@ package org.elasticsearch.common.joda; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.test.ESTestCase; import org.joda.time.DateTime; @@ -485,7 +485,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase { FormatDateTimeFormatter jodaFormatter = Joda.forPattern(format); DateTime jodaDateTime = jodaFormatter.parser().parseDateTime(input); - CompoundDateTimeFormatter javaTimeFormatter = DateFormatters.forPattern(format); + DateFormatter javaTimeFormatter = DateFormatters.forPattern(format); TemporalAccessor javaTimeAccessor = javaTimeFormatter.parse(input); ZonedDateTime zonedDateTime = DateFormatters.toZonedDateTime(javaTimeAccessor); @@ -507,7 +507,7 @@ public class JavaJodaTimeDuellingTests extends ESTestCase { } private void assertJavaTimeParseException(String input, String format, String expectedMessage) { - CompoundDateTimeFormatter javaTimeFormatter = DateFormatters.forPattern(format); + DateFormatter javaTimeFormatter = DateFormatters.forPattern(format); DateTimeParseException dateTimeParseException = expectThrows(DateTimeParseException.class, () -> javaTimeFormatter.parse(input)); assertThat(dateTimeParseException.getMessage(), startsWith(expectedMessage)); } diff --git a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java index f96674cf7a4..f01db140a70 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateFormattersTests.java @@ -31,17 +31,15 @@ import static org.hamcrest.Matchers.is; public class DateFormattersTests extends ESTestCase { - // the epoch milli parser is a bit special, as it does not use date formatter, see comments in DateFormatters public void testEpochMilliParser() { - CompoundDateTimeFormatter formatter = DateFormatters.forPattern("epoch_millis"); + DateFormatter formatter = DateFormatters.forPattern("epoch_millis"); DateTimeParseException e = expectThrows(DateTimeParseException.class, () -> formatter.parse("invalid")); assertThat(e.getMessage(), containsString("invalid number")); - // different zone, should still yield the same output, as epoch is time zoned independent + // different zone, should still yield the same output, as epoch is time zone independent ZoneId zoneId = randomZone(); - CompoundDateTimeFormatter zonedFormatter = formatter.withZone(zoneId); - assertThat(zonedFormatter.printer.getZone(), is(zoneId)); + DateFormatter zonedFormatter = formatter.withZone(zoneId); // test with negative and non negative values assertThatSameDateTime(formatter, zonedFormatter, randomNonNegativeLong() * -1); @@ -58,14 +56,21 @@ public class DateFormattersTests extends ESTestCase { assertSameFormat(formatter, 1); } - private void assertThatSameDateTime(CompoundDateTimeFormatter formatter, CompoundDateTimeFormatter zonedFormatter, long millis) { + public void testEpochMilliParsersWithDifferentFormatters() { + DateFormatter formatter = DateFormatters.forPattern("strict_date_optional_time||epoch_millis"); + TemporalAccessor accessor = formatter.parse("123"); + assertThat(DateFormatters.toZonedDateTime(accessor).toInstant().toEpochMilli(), is(123L)); + assertThat(formatter.pattern(), is("strict_date_optional_time||epoch_millis")); + } + + private void assertThatSameDateTime(DateFormatter formatter, DateFormatter zonedFormatter, long millis) { String millisAsString = String.valueOf(millis); ZonedDateTime formatterZonedDateTime = DateFormatters.toZonedDateTime(formatter.parse(millisAsString)); ZonedDateTime zonedFormatterZonedDateTime = DateFormatters.toZonedDateTime(zonedFormatter.parse(millisAsString)); assertThat(formatterZonedDateTime.toInstant().toEpochMilli(), is(zonedFormatterZonedDateTime.toInstant().toEpochMilli())); } - private void assertSameFormat(CompoundDateTimeFormatter formatter, long millis) { + private void assertSameFormat(DateFormatter formatter, long millis) { String millisAsString = String.valueOf(millis); TemporalAccessor accessor = formatter.parse(millisAsString); assertThat(millisAsString, is(formatter.format(accessor))); diff --git a/server/src/test/java/org/elasticsearch/common/time/DateMathParserTests.java b/server/src/test/java/org/elasticsearch/common/time/DateMathParserTests.java index d6dc7bb36f2..66e68b0aad0 100644 --- a/server/src/test/java/org/elasticsearch/common/time/DateMathParserTests.java +++ b/server/src/test/java/org/elasticsearch/common/time/DateMathParserTests.java @@ -35,7 +35,7 @@ import static org.hamcrest.Matchers.is; public class DateMathParserTests extends ESTestCase { - private final CompoundDateTimeFormatter formatter = DateFormatters.forPattern("dateOptionalTime||epoch_millis"); + private final DateFormatter formatter = DateFormatters.forPattern("dateOptionalTime||epoch_millis"); private final DateMathParser parser = new DateMathParser(formatter); public void testBasicDates() { @@ -138,7 +138,7 @@ public class DateMathParserTests extends ESTestCase { public void testRoundingPreservesEpochAsBaseDate() { // If a user only specifies times, then the date needs to always be 1970-01-01 regardless of rounding - CompoundDateTimeFormatter formatter = DateFormatters.forPattern("HH:mm:ss"); + DateFormatter formatter = DateFormatters.forPattern("HH:mm:ss"); DateMathParser parser = new DateMathParser(formatter); ZonedDateTime zonedDateTime = DateFormatters.toZonedDateTime(formatter.parse("04:52:20")); assertThat(zonedDateTime.getYear(), is(1970)); @@ -164,7 +164,7 @@ public class DateMathParserTests extends ESTestCase { assertDateMathEquals("2014-11-18T09:20", "2014-11-18T08:20:59.999Z", 0, true, ZoneId.of("CET")); // implicit rounding with explicit timezone in the date format - CompoundDateTimeFormatter formatter = DateFormatters.forPattern("yyyy-MM-ddXXX"); + DateFormatter formatter = DateFormatters.forPattern("yyyy-MM-ddXXX"); DateMathParser parser = new DateMathParser(formatter); long time = parser.parse("2011-10-09+01:00", () -> 0, false, null); assertEquals(this.parser.parse("2011-10-09T00:00:00.000+01:00", () -> 0), time); diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramTests.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramTests.java index cbc3424314a..ecd8868aabd 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramTests.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/histogram/DateHistogramTests.java @@ -26,7 +26,7 @@ import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.store.Directory; -import org.elasticsearch.common.time.CompoundDateTimeFormatter; +import org.elasticsearch.common.time.DateFormatter; import org.elasticsearch.common.time.DateFormatters; import org.elasticsearch.index.query.QueryShardContext; import org.elasticsearch.search.aggregations.BaseAggregationTestCase; @@ -137,7 +137,7 @@ public class DateHistogramTests extends BaseAggregationTestCase Date: Fri, 14 Sep 2018 15:25:53 +0300 Subject: [PATCH 34/45] Structured audit logging (#31931) Changes the format of log events in the audit logfile. It also changes the filename suffix from `_access` to `_audit`. The new entry format is consistent with Elastic Common Schema. Entries are formatted as JSON with no nested objects and field names have a dotted syntax. Moreover, log entries themselves are not spaced by commas and there is exactly one entry per line. In addition, entry fields are ordered, unlike a typical JSON doc, such that a human would not strain his eyes over jumbled fields from one line to the other; the order is defined in the log4j2 properties file. The implementation utilizes the log4j2's `StringMapMessage`. This means that the application builds the log event as a map and the log4j logic (the appender's layout) handle the format internally. The layout, such as the set of printed fields and their order, can be changed at runtime without restarting the node. --- .../core/src/main/config/log4j2.properties | 61 +- .../audit/logfile/CapturingLogger.java | 68 +- x-pack/plugin/security/build.gradle | 1 + .../audit/logfile/LoggingAuditTrail.java | 834 +++++++----- .../test/SecuritySettingsSource.java | 7 +- .../AuditTrailSettingsUpdateTests.java | 57 +- .../logfile/LoggingAuditTrailFilterTests.java | 24 +- .../audit/logfile/LoggingAuditTrailTests.java | 1201 ++++++++++------- .../authc/file/FileUserPasswdStoreTests.java | 8 +- .../authc/file/FileUserRolesStoreTests.java | 8 +- .../authc/support/DnRoleMapperTests.java | 12 +- .../authz/store/FileRolesStoreTests.java | 27 +- x-pack/qa/sql/security/build.gradle | 2 +- .../qa/sql/security/RestSqlSecurityIT.java | 12 +- .../qa/sql/security/SqlSecurityTestCase.java | 103 +- 15 files changed, 1467 insertions(+), 958 deletions(-) diff --git a/x-pack/plugin/core/src/main/config/log4j2.properties b/x-pack/plugin/core/src/main/config/log4j2.properties index c4cdbc0640c..1c9358f3cc4 100644 --- a/x-pack/plugin/core/src/main/config/log4j2.properties +++ b/x-pack/plugin/core/src/main/config/log4j2.properties @@ -1,9 +1,64 @@ appender.audit_rolling.type = RollingFile appender.audit_rolling.name = audit_rolling -appender.audit_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_access.log +appender.audit_rolling.fileName = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_audit.log appender.audit_rolling.layout.type = PatternLayout -appender.audit_rolling.layout.pattern = [%d{ISO8601}] %m%n -appender.audit_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_access-%d{yyyy-MM-dd}.log +appender.audit_rolling.layout.pattern = {\ + "@timestamp":"%d{ISO8601}"\ + %varsNotEmpty{, "node.name":"%enc{%map{node.name}}{JSON}"}\ + %varsNotEmpty{, "node.id":"%enc{%map{node.id}}{JSON}"}\ + %varsNotEmpty{, "host.name":"%enc{%map{host.name}}{JSON}"}\ + %varsNotEmpty{, "host.ip":"%enc{%map{host.ip}}{JSON}"}\ + %varsNotEmpty{, "event.type":"%enc{%map{event.type}}{JSON}"}\ + %varsNotEmpty{, "event.action":"%enc{%map{event.action}}{JSON}"}\ + %varsNotEmpty{, "user.name":"%enc{%map{user.name}}{JSON}"}\ + %varsNotEmpty{, "user.run_by.name":"%enc{%map{user.run_by.name}}{JSON}"}\ + %varsNotEmpty{, "user.run_as.name":"%enc{%map{user.run_as.name}}{JSON}"}\ + %varsNotEmpty{, "user.realm":"%enc{%map{user.realm}}{JSON}"}\ + %varsNotEmpty{, "user.run_by.realm":"%enc{%map{user.run_by.realm}}{JSON}"}\ + %varsNotEmpty{, "user.run_as.realm":"%enc{%map{user.run_as.realm}}{JSON}"}\ + %varsNotEmpty{, "user.roles":%map{user.roles}}\ + %varsNotEmpty{, "origin.type":"%enc{%map{origin.type}}{JSON}"}\ + %varsNotEmpty{, "origin.address":"%enc{%map{origin.address}}{JSON}"}\ + %varsNotEmpty{, "realm":"%enc{%map{realm}}{JSON}"}\ + %varsNotEmpty{, "url.path":"%enc{%map{url.path}}{JSON}"}\ + %varsNotEmpty{, "url.query":"%enc{%map{url.query}}{JSON}"}\ + %varsNotEmpty{, "request.body":"%enc{%map{request.body}}{JSON}"}\ + %varsNotEmpty{, "action":"%enc{%map{action}}{JSON}"}\ + %varsNotEmpty{, "request.name":"%enc{%map{request.name}}{JSON}"}\ + %varsNotEmpty{, "indices":%map{indices}}\ + %varsNotEmpty{, "opaque_id":"%enc{%map{opaque_id}}{JSON}"}\ + %varsNotEmpty{, "transport.profile":"%enc{%map{transport.profile}}{JSON}"}\ + %varsNotEmpty{, "rule":"%enc{%map{rule}}{JSON}"}\ + %varsNotEmpty{, "event.category":"%enc{%map{event.category}}{JSON}"}\ + }%n +# "node.name" node name from the `elasticsearch.yml` settings +# "node.id" node id which should not change between cluster restarts +# "host.name" unresolved hostname of the local node +# "host.ip" the local bound ip (i.e. the ip listening for connections) +# "event.type" a received REST request is translated into one or more transport requests. This indicates which processing layer generated the event "rest" or "transport" (internal) +# "event.action" the name of the audited event, eg. "authentication_failed", "access_granted", "run_as_granted", etc. +# "user.name" the subject name as authenticated by a realm +# "user.run_by.name" the original authenticated subject name that is impersonating another one. +# "user.run_as.name" if this "event.action" is of a run_as type, this is the subject name to be impersonated as. +# "user.realm" the name of the realm that authenticated "user.name" +# "user.run_by.realm" the realm name of the impersonating subject ("user.run_by.name") +# "user.run_as.realm" if this "event.action" is of a run_as type, this is the realm name the impersonated user is looked up from +# "user.roles" the roles array of the user; these are the roles that are granting privileges +# "origin.type" it is "rest" if the event is originating (is in relation to) a REST request; possible other values are "transport" and "ip_filter" +# "origin.address" the remote address and port of the first network hop, i.e. a REST proxy or another cluster node +# "realm" name of a realm that has generated an "authentication_failed" or an "authentication_successful"; the subject is not yet authenticated +# "url.path" the URI component between the port and the query string; it is percent (URL) encoded +# "url.query" the URI component after the path and before the fragment; it is percent (URL) encoded +# "request.body" the content of the request body entity, JSON escaped +# "action" an action is the most granular operation that is authorized and this identifies it in a namespaced way (internal) +# "request.name" if the event is in connection to a transport message this is the name of the request class, similar to how rest requests are identified by the url path (internal) +# "indices" the array of indices that the "action" is acting upon +# "opaque_id" opaque value conveyed by the "X-Opaque-Id" request header +# "transport.profile" name of the transport profile in case this is a "connection_granted" or "connection_denied" event +# "rule" name of the applied rulee if the "origin.type" is "ip_filter" +# "event.category" fixed value "elasticsearch-audit" + +appender.audit_rolling.filePattern = ${sys:es.logs.base_path}${sys:file.separator}${sys:es.logs.cluster_name}_audit-%d{yyyy-MM-dd}.log appender.audit_rolling.policies.type = Policies appender.audit_rolling.policies.time.type = TimeBasedTriggeringPolicy appender.audit_rolling.policies.time.interval = 1 diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/audit/logfile/CapturingLogger.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/audit/logfile/CapturingLogger.java index 2091f8fb75f..ede18c8241b 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/audit/logfile/CapturingLogger.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/audit/logfile/CapturingLogger.java @@ -8,26 +8,51 @@ package org.elasticsearch.xpack.core.security.audit.logfile; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.Layout; import org.apache.logging.log4j.core.LogEvent; import org.apache.logging.log4j.core.LoggerContext; +import org.apache.logging.log4j.core.StringLayout; import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.config.Configuration; import org.apache.logging.log4j.core.config.LoggerConfig; import org.apache.logging.log4j.core.filter.RegexFilter; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.logging.ESLoggerFactory; import org.elasticsearch.common.logging.Loggers; import java.util.ArrayList; import java.util.List; +/** + * Logger that captures events and appends them to in memory lists, with one + * list for each log level. This works with the global log manager context, + * meaning that there could only be a single logger with the same name. + */ public class CapturingLogger { - public static Logger newCapturingLogger(final Level level) throws IllegalAccessException { + /** + * Constructs a new {@link CapturingLogger} named as the fully qualified name of + * the invoking method. One name can be assigned to a single logger globally, so + * don't call this method multiple times in the same method. + * + * @param level + * The minimum priority level of events that will be captured. + * @param layout + * Optional parameter allowing to set the layout format of events. + * This is useful because events are captured to be inspected (and + * parsed) later. When parsing, it is useful to be in control of the + * printing format as well. If not specified, + * {@code event.getMessage().getFormattedMessage()} is called to + * format the event. + * @return The new logger. + */ + public static Logger newCapturingLogger(final Level level, @Nullable StringLayout layout) throws IllegalAccessException { + // careful, don't "bury" this on the call stack, unless you know what you're doing final StackTraceElement caller = Thread.currentThread().getStackTrace()[2]; final String name = caller.getClassName() + "." + caller.getMethodName() + "." + level.toString(); final Logger logger = ESLoggerFactory.getLogger(name); Loggers.setLevel(logger, level); - final MockAppender appender = new MockAppender(name); + final MockAppender appender = new MockAppender(name, layout); appender.start(); Loggers.addAppender(logger, appender); return logger; @@ -40,11 +65,27 @@ public class CapturingLogger { return (MockAppender) loggerConfig.getAppenders().get(name); } + /** + * Checks if the logger's appender has captured any events. + * + * @param name + * The unique global name of the logger. + * @return {@code true} if no event has been captured, {@code false} otherwise. + */ public static boolean isEmpty(final String name) { final MockAppender appender = getMockAppender(name); return appender.isEmpty(); } + /** + * Gets the captured events for a logger by its name. + * + * @param name + * The unique global name of the logger. + * @param level + * The priority level of the captured events to be returned. + * @return A list of captured events formated to {@code String}. + */ public static List output(final String name, final Level level) { final MockAppender appender = getMockAppender(name); return appender.output(level); @@ -58,8 +99,8 @@ public class CapturingLogger { public final List debug = new ArrayList<>(); public final List trace = new ArrayList<>(); - private MockAppender(final String name) throws IllegalAccessException { - super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), null); + private MockAppender(final String name, StringLayout layout) throws IllegalAccessException { + super(name, RegexFilter.createFilter(".*(\n.*)*", new String[0], false, null, null), layout); } @Override @@ -68,25 +109,34 @@ public class CapturingLogger { // we can not keep a reference to the event here because Log4j is using a thread // local instance under the hood case "ERROR": - error.add(event.getMessage().getFormattedMessage()); + error.add(formatMessage(event)); break; case "WARN": - warn.add(event.getMessage().getFormattedMessage()); + warn.add(formatMessage(event)); break; case "INFO": - info.add(event.getMessage().getFormattedMessage()); + info.add(formatMessage(event)); break; case "DEBUG": - debug.add(event.getMessage().getFormattedMessage()); + debug.add(formatMessage(event)); break; case "TRACE": - trace.add(event.getMessage().getFormattedMessage()); + trace.add(formatMessage(event)); break; default: throw invalidLevelException(event.getLevel()); } } + private String formatMessage(LogEvent event) { + final Layout layout = getLayout(); + if (layout instanceof StringLayout) { + return ((StringLayout) layout).toSerializable(event); + } else { + return event.getMessage().getFormattedMessage(); + } + } + private IllegalArgumentException invalidLevelException(Level level) { return new IllegalArgumentException("invalid level, expected [ERROR|WARN|INFO|DEBUG|TRACE] but was [" + level + "]"); } diff --git a/x-pack/plugin/security/build.gradle b/x-pack/plugin/security/build.gradle index 71b22531cca..74241be4a91 100644 --- a/x-pack/plugin/security/build.gradle +++ b/x-pack/plugin/security/build.gradle @@ -140,6 +140,7 @@ artifacts { } sourceSets.test.resources { srcDir '../core/src/test/resources' + srcDir '../core/src/main/config' } dependencyLicenses { mapping from: /java-support|opensaml-.*/, to: 'shibboleth' diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java index 5da6a9eb77c..040dee806e6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrail.java @@ -6,15 +6,16 @@ package org.elasticsearch.xpack.security.audit.logfile; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.StringMapMessage; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.cluster.ClusterChangedEvent; import org.elasticsearch.cluster.ClusterStateListener; import org.elasticsearch.cluster.node.DiscoveryNode; import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.component.AbstractComponent; -import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; @@ -37,11 +38,16 @@ import org.elasticsearch.xpack.security.audit.AuditTrail; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; +import com.fasterxml.jackson.core.io.JsonStringEncoder; + +import org.apache.logging.log4j.LogManager; + import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -52,7 +58,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; -import static org.elasticsearch.common.Strings.arrayToCommaDelimitedString; import static org.elasticsearch.xpack.core.security.SecurityField.setting; import static org.elasticsearch.xpack.security.audit.AuditLevel.ACCESS_DENIED; import static org.elasticsearch.xpack.security.audit.AuditLevel.ACCESS_GRANTED; @@ -71,55 +76,81 @@ import static org.elasticsearch.xpack.security.audit.AuditUtil.restRequestConten public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, ClusterStateListener { + public static final String REST_ORIGIN_FIELD_VALUE = "rest"; + public static final String LOCAL_ORIGIN_FIELD_VALUE = "local_node"; + public static final String TRANSPORT_ORIGIN_FIELD_VALUE = "transport"; + public static final String IP_FILTER_ORIGIN_FIELD_VALUE = "ip_filter"; + + // changing any of this names requires changing the log4j2.properties file too + public static final String ORIGIN_TYPE_FIELD_NAME = "origin.type"; + public static final String ORIGIN_ADDRESS_FIELD_NAME = "origin.address"; + public static final String NODE_NAME_FIELD_NAME = "node.name"; + public static final String NODE_ID_FIELD_NAME = "node.id"; + public static final String HOST_ADDRESS_FIELD_NAME = "host.ip"; + public static final String HOST_NAME_FIELD_NAME = "host.name"; + public static final String EVENT_TYPE_FIELD_NAME = "event.type"; + public static final String EVENT_ACTION_FIELD_NAME = "event.action"; + public static final String PRINCIPAL_FIELD_NAME = "user.name"; + public static final String PRINCIPAL_RUN_BY_FIELD_NAME = "user.run_by.name"; + public static final String PRINCIPAL_RUN_AS_FIELD_NAME = "user.run_as.name"; + public static final String PRINCIPAL_REALM_FIELD_NAME = "user.realm"; + public static final String PRINCIPAL_RUN_BY_REALM_FIELD_NAME = "user.run_by.realm"; + public static final String PRINCIPAL_RUN_AS_REALM_FIELD_NAME = "user.run_as.realm"; + public static final String PRINCIPAL_ROLES_FIELD_NAME = "user.roles"; + public static final String REALM_FIELD_NAME = "realm"; + public static final String URL_PATH_FIELD_NAME = "url.path"; + public static final String URL_QUERY_FIELD_NAME = "url.query"; + public static final String REQUEST_BODY_FIELD_NAME = "request.body"; + public static final String ACTION_FIELD_NAME = "action"; + public static final String INDICES_FIELD_NAME = "indices"; + public static final String REQUEST_NAME_FIELD_NAME = "request.name"; + public static final String TRANSPORT_PROFILE_FIELD_NAME = "transport.profile"; + public static final String RULE_FIELD_NAME = "rule"; + public static final String OPAQUE_ID_FIELD_NAME = "opaque_id"; + public static final String NAME = "logfile"; - public static final Setting HOST_ADDRESS_SETTING = - Setting.boolSetting(setting("audit.logfile.prefix.emit_node_host_address"), false, Property.NodeScope, Property.Dynamic); - public static final Setting HOST_NAME_SETTING = - Setting.boolSetting(setting("audit.logfile.prefix.emit_node_host_name"), false, Property.NodeScope, Property.Dynamic); - public static final Setting NODE_NAME_SETTING = - Setting.boolSetting(setting("audit.logfile.prefix.emit_node_name"), true, Property.NodeScope, Property.Dynamic); - private static final List DEFAULT_EVENT_INCLUDES = Arrays.asList( - ACCESS_DENIED.toString(), - ACCESS_GRANTED.toString(), - ANONYMOUS_ACCESS_DENIED.toString(), - AUTHENTICATION_FAILED.toString(), - CONNECTION_DENIED.toString(), - TAMPERED_REQUEST.toString(), - RUN_AS_DENIED.toString(), - RUN_AS_GRANTED.toString() - ); - public static final Setting> INCLUDE_EVENT_SETTINGS = - Setting.listSetting(setting("audit.logfile.events.include"), DEFAULT_EVENT_INCLUDES, Function.identity(), Property.NodeScope, - Property.Dynamic); - public static final Setting> EXCLUDE_EVENT_SETTINGS = - Setting.listSetting(setting("audit.logfile.events.exclude"), Collections.emptyList(), Function.identity(), Property.NodeScope, - Property.Dynamic); - public static final Setting INCLUDE_REQUEST_BODY = - Setting.boolSetting(setting("audit.logfile.events.emit_request_body"), false, Property.NodeScope, Property.Dynamic); + public static final Setting EMIT_HOST_ADDRESS_SETTING = Setting + .boolSetting(setting("audit.logfile.prefix.emit_node_host_address"), false, Property.NodeScope, Property.Dynamic); + public static final Setting EMIT_HOST_NAME_SETTING = Setting.boolSetting(setting("audit.logfile.prefix.emit_node_host_name"), + false, Property.NodeScope, Property.Dynamic); + public static final Setting EMIT_NODE_NAME_SETTING = Setting.boolSetting(setting("audit.logfile.prefix.emit_node_name"), false, + Property.NodeScope, Property.Dynamic); + public static final Setting EMIT_NODE_ID_SETTING = Setting.boolSetting(setting("audit.logfile.prefix.emit_node_id"), true, + Property.NodeScope, Property.Dynamic); + private static final List DEFAULT_EVENT_INCLUDES = Arrays.asList(ACCESS_DENIED.toString(), ACCESS_GRANTED.toString(), + ANONYMOUS_ACCESS_DENIED.toString(), AUTHENTICATION_FAILED.toString(), CONNECTION_DENIED.toString(), TAMPERED_REQUEST.toString(), + RUN_AS_DENIED.toString(), RUN_AS_GRANTED.toString()); + public static final Setting> INCLUDE_EVENT_SETTINGS = Setting.listSetting(setting("audit.logfile.events.include"), + DEFAULT_EVENT_INCLUDES, Function.identity(), Property.NodeScope, Property.Dynamic); + public static final Setting> EXCLUDE_EVENT_SETTINGS = Setting.listSetting(setting("audit.logfile.events.exclude"), + Collections.emptyList(), Function.identity(), Property.NodeScope, Property.Dynamic); + public static final Setting INCLUDE_REQUEST_BODY = Setting.boolSetting(setting("audit.logfile.events.emit_request_body"), + false, Property.NodeScope, Property.Dynamic); private static final String FILTER_POLICY_PREFIX = setting("audit.logfile.events.ignore_filters."); // because of the default wildcard value (*) for the field filter, a policy with // an unspecified filter field will match events that have any value for that // particular field, as well as events with that particular field missing - private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_PRINCIPALS = - Setting.affixKeySetting(FILTER_POLICY_PREFIX, "users", (key) -> Setting.listSetting(key, Collections.singletonList("*"), - Function.identity(), Property.NodeScope, Property.Dynamic)); - private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_REALMS = - Setting.affixKeySetting(FILTER_POLICY_PREFIX, "realms", (key) -> Setting.listSetting(key, Collections.singletonList("*"), - Function.identity(), Property.NodeScope, Property.Dynamic)); - private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_ROLES = - Setting.affixKeySetting(FILTER_POLICY_PREFIX, "roles", (key) -> Setting.listSetting(key, Collections.singletonList("*"), - Function.identity(), Property.NodeScope, Property.Dynamic)); - private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_INDICES = - Setting.affixKeySetting(FILTER_POLICY_PREFIX, "indices", (key) -> Setting.listSetting(key, Collections.singletonList("*"), - Function.identity(), Property.NodeScope, Property.Dynamic)); + private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_PRINCIPALS = Setting.affixKeySetting(FILTER_POLICY_PREFIX, + "users", + (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), Property.NodeScope, Property.Dynamic)); + private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_REALMS = Setting.affixKeySetting(FILTER_POLICY_PREFIX, + "realms", + (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), Property.NodeScope, Property.Dynamic)); + private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_ROLES = Setting.affixKeySetting(FILTER_POLICY_PREFIX, + "roles", + (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), Property.NodeScope, Property.Dynamic)); + private static final Setting.AffixSetting> FILTER_POLICY_IGNORE_INDICES = Setting.affixKeySetting(FILTER_POLICY_PREFIX, + "indices", + (key) -> Setting.listSetting(key, Collections.singletonList("*"), Function.identity(), Property.NodeScope, Property.Dynamic)); private final Logger logger; - final EventFilterPolicyRegistry eventFilterPolicyRegistry; private final ThreadContext threadContext; + final EventFilterPolicyRegistry eventFilterPolicyRegistry; // package for testing volatile EnumSet events; boolean includeRequestBody; - LocalNodeInfo localNodeInfo; + // fields that all entries have in common + EntryCommonFields entryCommonFields; @Override public String name() { @@ -127,7 +158,7 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, } public LoggingAuditTrail(Settings settings, ClusterService clusterService, ThreadPool threadPool) { - this(settings, clusterService, Loggers.getLogger(LoggingAuditTrail.class), threadPool.getThreadContext()); + this(settings, clusterService, LogManager.getLogger(), threadPool.getThreadContext()); } LoggingAuditTrail(Settings settings, ClusterService clusterService, Logger logger, ThreadContext threadContext) { @@ -136,20 +167,18 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, this.events = parse(INCLUDE_EVENT_SETTINGS.get(settings), EXCLUDE_EVENT_SETTINGS.get(settings)); this.includeRequestBody = INCLUDE_REQUEST_BODY.get(settings); this.threadContext = threadContext; - this.localNodeInfo = new LocalNodeInfo(settings, null); + this.entryCommonFields = new EntryCommonFields(settings, null); this.eventFilterPolicyRegistry = new EventFilterPolicyRegistry(settings); clusterService.addListener(this); clusterService.getClusterSettings().addSettingsUpdateConsumer(newSettings -> { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - final Settings.Builder builder = Settings.builder().put(localNodeInfo.settings).put(newSettings, false); - this.localNodeInfo = new LocalNodeInfo(builder.build(), localNodeInfo.localNode); + this.entryCommonFields = this.entryCommonFields.withNewSettings(newSettings); this.includeRequestBody = INCLUDE_REQUEST_BODY.get(newSettings); // `events` is a volatile field! Keep `events` write last so that - // `localNodeInfo` and `includeRequestBody` writes happen-before! `events` is - // always read before `localNodeInfo` and `includeRequestBody`. + // `entryCommonFields` and `includeRequestBody` writes happen-before! `events` is + // always read before `entryCommonFields` and `includeRequestBody`. this.events = parse(INCLUDE_EVENT_SETTINGS.get(newSettings), EXCLUDE_EVENT_SETTINGS.get(newSettings)); - }, Arrays.asList(HOST_ADDRESS_SETTING, HOST_NAME_SETTING, NODE_NAME_SETTING, INCLUDE_EVENT_SETTINGS, EXCLUDE_EVENT_SETTINGS, - INCLUDE_REQUEST_BODY)); + }, Arrays.asList(EMIT_HOST_ADDRESS_SETTING, EMIT_HOST_NAME_SETTING, EMIT_NODE_NAME_SETTING, EMIT_NODE_ID_SETTING, + INCLUDE_EVENT_SETTINGS, EXCLUDE_EVENT_SETTINGS, INCLUDE_REQUEST_BODY)); clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_PRINCIPALS, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)) @@ -158,35 +187,36 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_REALMS, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); - final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)) - .changeRealmsFilter(filtersList); + final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)).changeRealmsFilter(filtersList); this.eventFilterPolicyRegistry.set(policyName, newPolicy); }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_ROLES, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); - final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)) - .changeRolesFilter(filtersList); + final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)).changeRolesFilter(filtersList); this.eventFilterPolicyRegistry.set(policyName, newPolicy); }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); clusterService.getClusterSettings().addAffixUpdateConsumer(FILTER_POLICY_IGNORE_INDICES, (policyName, filtersList) -> { final Optional policy = eventFilterPolicyRegistry.get(policyName); - final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)) - .changeIndicesFilter(filtersList); + final EventFilterPolicy newPolicy = policy.orElse(new EventFilterPolicy(policyName, settings)).changeIndicesFilter(filtersList); this.eventFilterPolicyRegistry.set(policyName, newPolicy); }, (policyName, filtersList) -> EventFilterPolicy.parsePredicate(filtersList)); } @Override public void authenticationSuccess(String realm, User user, RestRequest request) { - if (events.contains(AUTHENTICATION_SUCCESS) && (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(user), Optional.of(realm), Optional.empty(), Optional.empty())) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [authentication_success]\t{}, realm=[{}], uri=[{}], params=[{}]{}, request_body=[{}]", - localNodeInfo.prefix, principal(user), realm, request.uri(), request.params(), opaqueId(), restRequestContent(request)); - } else { - logger.info("{}[rest] [authentication_success]\t{}, realm=[{}], uri=[{}], params=[{}]{}", - localNodeInfo.prefix, principal(user), realm, request.uri(), request.params(), opaqueId()); - } + if (events.contains(AUTHENTICATION_SUCCESS) && eventFilterPolicyRegistry.ignorePredicate() + .test(new AuditEventMetaInfo(Optional.of(user), Optional.of(realm), Optional.empty(), Optional.empty())) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_success") + .with(REALM_FIELD_NAME, realm) + .withRestUri(request) + .withPrincipal(user) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -196,16 +226,18 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(user), Optional.of(realm), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [authentication_success]\t{}, {}, realm=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), principal(user), realm, action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [authentication_success]\t{}, {}, realm=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), principal(user), realm, action, - message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_success") + .with(REALM_FIELD_NAME, realm) + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withPrincipal(user) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @@ -216,16 +248,16 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [anonymous_access_denied]\t{}, action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [anonymous_access_denied]\t{}, action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), action, - message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @@ -233,14 +265,16 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, @Override public void anonymousAccessDenied(RestRequest request) { if (events.contains(ANONYMOUS_ACCESS_DENIED) - && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [anonymous_access_denied]\t{}, uri=[{}]{}, request_body=[{}]", localNodeInfo.prefix, - hostAttributes(request), request.uri(), opaqueId(), restRequestContent(request)); - } else { - logger.info("{}[rest] [anonymous_access_denied]\t{}, uri=[{}]{}", localNodeInfo.prefix, - hostAttributes(request), request.uri(), opaqueId()); - } + && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") + .withRestUri(request) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -250,31 +284,33 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [authentication_failed]\t{}, principal=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), token.principal(), action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [authentication_failed]\t{}, principal=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), token.principal(), action, - message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") + .with(ACTION_FIELD_NAME, action) + .with(PRINCIPAL_FIELD_NAME, token.principal()) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override public void authenticationFailed(RestRequest request) { - if (events.contains(AUTHENTICATION_FAILED) - && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [authentication_failed]\t{}, uri=[{}]{}, request_body=[{}]", localNodeInfo.prefix, - hostAttributes(request), request.uri(), opaqueId(), restRequestContent(request)); - } else { - logger.info("{}[rest] [authentication_failed]\t{}, uri=[{}]{}", localNodeInfo.prefix, - hostAttributes(request), request.uri(), opaqueId()); - } + if (events.contains(AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") + .withRestUri(request) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -284,33 +320,34 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [authentication_failed]\t{}, action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [authentication_failed]\t{}, action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), action, - message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override public void authenticationFailed(AuthenticationToken token, RestRequest request) { - if (events.contains(AUTHENTICATION_FAILED) - && (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty())) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [authentication_failed]\t{}, principal=[{}], uri=[{}]{}, request_body=[{}]", - localNodeInfo.prefix, hostAttributes(request), token.principal(), request.uri(), opaqueId(), - restRequestContent(request)); - } else { - logger.info("{}[rest] [authentication_failed]\t{}, principal=[{}], uri=[{}]{}", - localNodeInfo.prefix, hostAttributes(request), token.principal(), request.uri(), opaqueId()); - } + if (events.contains(AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() + .test(new AuditEventMetaInfo(Optional.of(token), Optional.empty(), Optional.empty())) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "authentication_failed") + .with(PRINCIPAL_FIELD_NAME, token.principal()) + .withRestUri(request) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -320,36 +357,37 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info( - "{}[transport] [realm_authentication_failed]\trealm=[{}], {}, principal=[{}], action=[{}], indices=[{}], " - + "request=[{}]{}", - localNodeInfo.prefix, realm, originAttributes(threadContext, message, localNodeInfo), token.principal(), action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [realm_authentication_failed]\trealm=[{}], {}, principal=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, realm, originAttributes(threadContext, message, localNodeInfo), token.principal(), action, - message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") + .with(REALM_FIELD_NAME, realm) + .with(PRINCIPAL_FIELD_NAME, token.principal()) + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override public void authenticationFailed(String realm, AuthenticationToken token, RestRequest request) { - if (events.contains(REALM_AUTHENTICATION_FAILED) - && (eventFilterPolicyRegistry.ignorePredicate() - .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty())) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [realm_authentication_failed]\trealm=[{}], {}, principal=[{}], uri=[{}]{}, " - + "request_body=[{}]", - localNodeInfo.prefix, realm, hostAttributes(request), token.principal(), request.uri(), opaqueId(), - restRequestContent(request)); - } else { - logger.info("{}[rest] [realm_authentication_failed]\trealm=[{}], {}, principal=[{}], uri=[{}]{}", - localNodeInfo.prefix, realm, hostAttributes(request), token.principal(), request.uri(), opaqueId()); - } + if (events.contains(REALM_AUTHENTICATION_FAILED) && eventFilterPolicyRegistry.ignorePredicate() + .test(new AuditEventMetaInfo(Optional.of(token), Optional.of(realm), Optional.empty())) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") + .with(REALM_FIELD_NAME, realm) + .with(PRINCIPAL_FIELD_NAME, token.principal()) + .withRestUri(request) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -361,17 +399,18 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(user), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [access_granted]\t{}, {}, roles=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), subject(authentication), - arrayToCommaDelimitedString(roleNames), action, arrayToCommaDelimitedString(indices.get()), - message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [access_granted]\t{}, {}, roles=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), subject(authentication), - arrayToCommaDelimitedString(roleNames), action, message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "access_granted") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withSubject(authentication) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @@ -382,31 +421,34 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [access_denied]\t{}, {}, roles=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), subject(authentication), - arrayToCommaDelimitedString(roleNames), action, arrayToCommaDelimitedString(indices.get()), - message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [access_denied]\t{}, {}, roles=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), subject(authentication), - arrayToCommaDelimitedString(roleNames), action, message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "access_denied") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withSubject(authentication) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override public void tamperedRequest(RestRequest request) { - if (events.contains(TAMPERED_REQUEST) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [tampered_request]\t{}, uri=[{}]{}, request_body=[{}]", localNodeInfo.prefix, - hostAttributes(request), request.uri(), opaqueId(), restRequestContent(request)); - } else { - logger.info("{}[rest] [tampered_request]\t{}, uri=[{}]{}", localNodeInfo.prefix, hostAttributes(request), - request.uri(), opaqueId()); - } + if (events.contains(TAMPERED_REQUEST) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "tampered_request") + .withRestUri(request) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -416,53 +458,70 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.empty(), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [tampered_request]\t{}, action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), action, - arrayToCommaDelimitedString(indices.get()), message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [tampered_request]\t{}, action=[{}], request=[{}]{}", localNodeInfo.prefix, - originAttributes(threadContext, message, localNodeInfo), action, message.getClass().getSimpleName(), - opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "tampered_request") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override - public void tamperedRequest(User user, String action, TransportMessage request) { + public void tamperedRequest(User user, String action, TransportMessage message) { if (events.contains(TAMPERED_REQUEST)) { - final Optional indices = indices(request); + final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate() .test(new AuditEventMetaInfo(Optional.of(user), Optional.empty(), Optional.empty(), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [tampered_request]\t{}, {}, action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, request, localNodeInfo), principal(user), action, - arrayToCommaDelimitedString(indices.get()), request.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [tampered_request]\t{}, {}, action=[{}], request=[{}]{}", localNodeInfo.prefix, - originAttributes(threadContext, request, localNodeInfo), principal(user), action, - request.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "tampered_request") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRestOrTransportOrigin(message, threadContext) + .withPrincipal(user) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @Override public void connectionGranted(InetAddress inetAddress, String profile, SecurityIpFilterRule rule) { - if (events.contains(CONNECTION_GRANTED) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { - logger.info("{}[ip_filter] [connection_granted]\torigin_address=[{}], transport_profile=[{}], rule=[{}]{}", - localNodeInfo.prefix, NetworkAddress.format(inetAddress), profile, rule, opaqueId()); + if (events.contains(CONNECTION_GRANTED) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, IP_FILTER_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "connection_granted") + .with(ORIGIN_TYPE_FIELD_NAME, IP_FILTER_ORIGIN_FIELD_VALUE) + .with(ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(inetAddress)) + .with(TRANSPORT_PROFILE_FIELD_NAME, profile) + .with(RULE_FIELD_NAME, rule.toString()) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @Override public void connectionDenied(InetAddress inetAddress, String profile, SecurityIpFilterRule rule) { - if (events.contains(CONNECTION_DENIED) && (eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false)) { - logger.info("{}[ip_filter] [connection_denied]\torigin_address=[{}], transport_profile=[{}], rule=[{}]{}", - localNodeInfo.prefix, NetworkAddress.format(inetAddress), profile, rule, opaqueId()); + if (events.contains(CONNECTION_DENIED) && eventFilterPolicyRegistry.ignorePredicate().test(AuditEventMetaInfo.EMPTY) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, IP_FILTER_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "connection_denied") + .with(ORIGIN_TYPE_FIELD_NAME, IP_FILTER_ORIGIN_FIELD_VALUE) + .with(ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(inetAddress)) + .with(TRANSPORT_PROFILE_FIELD_NAME, profile) + .with(RULE_FIELD_NAME, rule.toString()) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } @@ -472,17 +531,18 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [run_as_granted]\t{}, {}, roles=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), runAsSubject(authentication), - arrayToCommaDelimitedString(roleNames), action, arrayToCommaDelimitedString(indices.get()), - message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [run_as_granted]\t{}, {}, roles=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), runAsSubject(authentication), - arrayToCommaDelimitedString(roleNames), action, message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "run_as_granted") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRunAsSubject(authentication) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @@ -493,17 +553,18 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, final Optional indices = indices(message); if (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), indices)) == false) { - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if (indices.isPresent()) { - logger.info("{}[transport] [run_as_denied]\t{}, {}, roles=[{}], action=[{}], indices=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), runAsSubject(authentication), - arrayToCommaDelimitedString(roleNames), action, arrayToCommaDelimitedString(indices.get()), - message.getClass().getSimpleName(), opaqueId()); - } else { - logger.info("{}[transport] [run_as_denied]\t{}, {}, roles=[{}], action=[{}], request=[{}]{}", - localNodeInfo.prefix, originAttributes(threadContext, message, localNodeInfo), runAsSubject(authentication), - arrayToCommaDelimitedString(roleNames), action, message.getClass().getSimpleName(), opaqueId()); - } + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") + .with(ACTION_FIELD_NAME, action) + .with(REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()) + .withRunAsSubject(authentication) + .withRestOrTransportOrigin(message, threadContext) + .with(INDICES_FIELD_NAME, indices.orElse(null)) + .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); } } } @@ -511,113 +572,183 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, @Override public void runAsDenied(Authentication authentication, RestRequest request, String[] roleNames) { if (events.contains(RUN_AS_DENIED) - && (eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), - Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), Optional.empty())) == false)) { - if (includeRequestBody) { - logger.info("{}[rest] [run_as_denied]\t{}, {}, roles=[{}], uri=[{}], request_body=[{}]{}", - localNodeInfo.prefix, hostAttributes(request), runAsSubject(authentication), - arrayToCommaDelimitedString(roleNames), request.uri(), restRequestContent(request), opaqueId()); - } else { - logger.info("{}[rest] [run_as_denied]\t{}, {}, roles=[{}], uri=[{}]{}", localNodeInfo.prefix, - hostAttributes(request), runAsSubject(authentication), arrayToCommaDelimitedString(roleNames), request.uri(), - opaqueId()); + && eventFilterPolicyRegistry.ignorePredicate().test(new AuditEventMetaInfo(Optional.of(authentication.getUser()), + Optional.of(effectiveRealmName(authentication)), Optional.of(roleNames), Optional.empty())) == false) { + final StringMapMessage logEntry = new LogEntryBuilder() + .with(EVENT_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(EVENT_ACTION_FIELD_NAME, "run_as_denied") + .with(PRINCIPAL_ROLES_FIELD_NAME, roleNames) + .withRestUri(request) + .withRunAsSubject(authentication) + .withRestOrigin(request) + .withRequestBody(request) + .withOpaqueId(threadContext) + .build(); + logger.info(logEntry); + } + } + + private class LogEntryBuilder { + + private final StringMapMessage logEntry; + + LogEntryBuilder() { + logEntry = new StringMapMessage(LoggingAuditTrail.this.entryCommonFields.commonFields); + } + + LogEntryBuilder withRestUri(RestRequest request) { + final int queryStringIndex = request.uri().indexOf('?'); + int queryStringLength = request.uri().indexOf('#'); + if (queryStringLength < 0) { + queryStringLength = request.uri().length(); } + if (queryStringIndex < 0) { + logEntry.with(URL_PATH_FIELD_NAME, request.uri().substring(0, queryStringLength)); + } else { + logEntry.with(URL_PATH_FIELD_NAME, request.uri().substring(0, queryStringIndex)); + } + if (queryStringIndex > -1) { + logEntry.with(URL_QUERY_FIELD_NAME, request.uri().substring(queryStringIndex + 1, queryStringLength)); + } + return this; } - } - static String runAsSubject(Authentication authentication) { - final StringBuilder sb = new StringBuilder("principal=["); - sb.append(authentication.getUser().authenticatedUser().principal()); - sb.append("], realm=["); - sb.append(authentication.getAuthenticatedBy().getName()); - sb.append("], run_as_principal=["); - sb.append(authentication.getUser().principal()); - if (authentication.getLookedUpBy() != null) { - sb.append("], run_as_realm=[").append(authentication.getLookedUpBy().getName()); + LogEntryBuilder withRunAsSubject(Authentication authentication) { + logEntry.with(PRINCIPAL_FIELD_NAME, authentication.getUser().authenticatedUser().principal()) + .with(PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName()) + .with(PRINCIPAL_RUN_AS_FIELD_NAME, authentication.getUser().principal()); + if (authentication.getLookedUpBy() != null) { + logEntry.with(PRINCIPAL_RUN_AS_REALM_FIELD_NAME, authentication.getLookedUpBy().getName()); + } + return this; } - sb.append("]"); - return sb.toString(); - } - static String subject(Authentication authentication) { - final StringBuilder sb = new StringBuilder("principal=["); - sb.append(authentication.getUser().principal()).append("], realm=["); - if (authentication.getUser().isRunAs()) { - sb.append(authentication.getLookedUpBy().getName()).append("], run_by_principal=["); - sb.append(authentication.getUser().authenticatedUser().principal()).append("], run_by_realm=["); + LogEntryBuilder withRestOrigin(RestRequest request) { + assert LOCAL_ORIGIN_FIELD_VALUE.equals(logEntry.get(ORIGIN_TYPE_FIELD_NAME)); // this is the default + final InetSocketAddress socketAddress = request.getHttpChannel().getRemoteAddress(); + if (socketAddress != null) { + logEntry.with(ORIGIN_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(socketAddress)); + } + // fall through to local_node default + return this; } - sb.append(authentication.getAuthenticatedBy().getName()).append("]"); - return sb.toString(); - } - private static String hostAttributes(RestRequest request) { - final InetSocketAddress socketAddress = request.getHttpChannel().getRemoteAddress(); - String formattedAddress = NetworkAddress.format(socketAddress.getAddress()); - return "origin_address=[" + formattedAddress + "]"; - } - - protected static String originAttributes(ThreadContext threadContext, TransportMessage message, LocalNodeInfo localNodeInfo) { - return restOriginTag(threadContext).orElse(transportOriginTag(message).orElse(localNodeInfo.localOriginTag)); - } - - private String opaqueId() { - String opaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); - if (opaqueId != null) { - return ", opaque_id=[" + opaqueId + "]"; - } else { - return ""; + LogEntryBuilder withRestOrTransportOrigin(TransportMessage message, ThreadContext threadContext) { + assert LOCAL_ORIGIN_FIELD_VALUE.equals(logEntry.get(ORIGIN_TYPE_FIELD_NAME)); // this is the default + final InetSocketAddress restAddress = RemoteHostHeader.restRemoteAddress(threadContext); + if (restAddress != null) { + logEntry.with(ORIGIN_TYPE_FIELD_NAME, REST_ORIGIN_FIELD_VALUE) + .with(ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(restAddress)); + } else { + final TransportAddress address = message.remoteAddress(); + if (address != null) { + logEntry.with(ORIGIN_TYPE_FIELD_NAME, TRANSPORT_ORIGIN_FIELD_VALUE) + .with(ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address.address())); + } + } + // fall through to local_node default + return this; } - } - private static Optional restOriginTag(ThreadContext threadContext) { - final InetSocketAddress restAddress = RemoteHostHeader.restRemoteAddress(threadContext); - if (restAddress == null) { - return Optional.empty(); + LogEntryBuilder withRequestBody(RestRequest request) { + if (includeRequestBody) { + final String requestContent = restRequestContent(request); + if (Strings.hasLength(requestContent)) { + logEntry.with(REQUEST_BODY_FIELD_NAME, requestContent); + } + } + return this; } - return Optional.of(new StringBuilder("origin_type=[rest], origin_address=[").append(NetworkAddress.format(restAddress.getAddress())) - .append("]") - .toString()); - } - private static Optional transportOriginTag(TransportMessage message) { - final TransportAddress address = message.remoteAddress(); - if (address == null) { - return Optional.empty(); + LogEntryBuilder withOpaqueId(ThreadContext threadContext) { + final String opaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); + if (opaqueId != null) { + logEntry.with(OPAQUE_ID_FIELD_NAME, opaqueId); + } + return this; } - return Optional.of( - new StringBuilder("origin_type=[transport], origin_address=[").append(NetworkAddress.format(address.address().getAddress())) - .append("]") - .toString()); + + LogEntryBuilder withPrincipal(User user) { + logEntry.with(PRINCIPAL_FIELD_NAME, user.principal()); + if (user.isRunAs()) { + logEntry.with(PRINCIPAL_RUN_BY_FIELD_NAME, user.authenticatedUser().principal()); + } + return this; + } + + LogEntryBuilder withSubject(Authentication authentication) { + logEntry.with(PRINCIPAL_FIELD_NAME, authentication.getUser().principal()); + if (authentication.getUser().isRunAs()) { + logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getLookedUpBy().getName()) + .with(PRINCIPAL_RUN_BY_FIELD_NAME, authentication.getUser().authenticatedUser().principal()) + .with(PRINCIPAL_RUN_BY_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName()); + } else { + logEntry.with(PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName()); + } + return this; + } + + LogEntryBuilder with(String key, String value) { + if (value != null) { + logEntry.with(key, value); + } + return this; + } + + LogEntryBuilder with(String key, String[] values) { + if (values != null) { + logEntry.with(key, toQuotedJsonArray(values)); + } + return this; + } + + StringMapMessage build() { + return logEntry; + } + + String toQuotedJsonArray(String[] values) { + assert values != null; + final StringBuilder stringBuilder = new StringBuilder(); + final JsonStringEncoder jsonStringEncoder = JsonStringEncoder.getInstance(); + stringBuilder.append("["); + for (final String value : values) { + if (value != null) { + if (stringBuilder.length() > 1) { + stringBuilder.append(","); + } + stringBuilder.append("\""); + jsonStringEncoder.quoteAsString(value, stringBuilder); + stringBuilder.append("\""); + } + } + stringBuilder.append("]"); + return stringBuilder.toString(); + } + } - static Optional indices(TransportMessage message) { + + private static Optional indices(TransportMessage message) { if (message instanceof IndicesRequest) { final String[] indices = ((IndicesRequest) message).indices(); - if ((indices != null) && (indices.length != 0)) { + if (indices != null) { return Optional.of(((IndicesRequest) message).indices()); } } return Optional.empty(); } - static String effectiveRealmName(Authentication authentication) { + private static String effectiveRealmName(Authentication authentication) { return authentication.getLookedUpBy() != null ? authentication.getLookedUpBy().getName() : authentication.getAuthenticatedBy().getName(); } - static String principal(User user) { - final StringBuilder builder = new StringBuilder("principal=["); - builder.append(user.principal()); - if (user.isRunAs()) { - builder.append("], run_by_principal=[").append(user.authenticatedUser().principal()); - } - return builder.append("]").toString(); - } - public static void registerSettings(List> settings) { - settings.add(HOST_ADDRESS_SETTING); - settings.add(HOST_NAME_SETTING); - settings.add(NODE_NAME_SETTING); + settings.add(EMIT_HOST_ADDRESS_SETTING); + settings.add(EMIT_HOST_NAME_SETTING); + settings.add(EMIT_NODE_NAME_SETTING); + settings.add(EMIT_NODE_ID_SETTING); settings.add(INCLUDE_EVENT_SETTINGS); settings.add(EXCLUDE_EVENT_SETTINGS); settings.add(INCLUDE_REQUEST_BODY); @@ -813,56 +944,53 @@ public class LoggingAuditTrail extends AbstractComponent implements AuditTrail, void updateLocalNodeInfo(DiscoveryNode newLocalNode) { // check if local node changed - final LocalNodeInfo localNodeInfo = this.localNodeInfo; - if ((localNodeInfo.localNode == null) || (localNodeInfo.localNode.equals(newLocalNode) == false)) { + final EntryCommonFields localNodeInfo = this.entryCommonFields; + if (localNodeInfo.localNode == null || localNodeInfo.localNode.equals(newLocalNode) == false) { // no need to synchronize, called only from the cluster state applier thread - this.localNodeInfo = new LocalNodeInfo(localNodeInfo.settings, newLocalNode); + this.entryCommonFields = this.entryCommonFields.withNewLocalNode(newLocalNode); } } - static class LocalNodeInfo { + static class EntryCommonFields { private final Settings settings; private final DiscoveryNode localNode; - final String prefix; - private final String localOriginTag; + final Map commonFields; - LocalNodeInfo(Settings settings, @Nullable DiscoveryNode newLocalNode) { + EntryCommonFields(Settings settings, @Nullable DiscoveryNode newLocalNode) { this.settings = settings; this.localNode = newLocalNode; - this.prefix = resolvePrefix(settings, newLocalNode); - this.localOriginTag = localOriginTag(newLocalNode); + final Map commonFields = new HashMap<>(); + if (EMIT_NODE_NAME_SETTING.get(settings)) { + final String nodeName = Node.NODE_NAME_SETTING.get(settings); + if (Strings.hasLength(nodeName)) { + commonFields.put(NODE_NAME_FIELD_NAME, nodeName); + } + } + if (newLocalNode != null && newLocalNode.getAddress() != null) { + if (EMIT_HOST_ADDRESS_SETTING.get(settings)) { + commonFields.put(HOST_ADDRESS_FIELD_NAME, newLocalNode.getAddress().getAddress()); + } + if (EMIT_HOST_NAME_SETTING.get(settings)) { + commonFields.put(HOST_NAME_FIELD_NAME, newLocalNode.getAddress().address().getHostString()); + } + if (EMIT_NODE_ID_SETTING.get(settings)) { + commonFields.put(NODE_ID_FIELD_NAME, newLocalNode.getId()); + } + // the default origin is local + commonFields.put(ORIGIN_ADDRESS_FIELD_NAME, newLocalNode.getAddress().toString()); + } + // the default origin is local + commonFields.put(ORIGIN_TYPE_FIELD_NAME, LOCAL_ORIGIN_FIELD_VALUE); + this.commonFields = Collections.unmodifiableMap(commonFields); } - static String resolvePrefix(Settings settings, @Nullable DiscoveryNode localNode) { - final StringBuilder builder = new StringBuilder(); - if (HOST_ADDRESS_SETTING.get(settings)) { - final String address = localNode != null ? localNode.getHostAddress() : null; - if (address != null) { - builder.append("[").append(address).append("] "); - } - } - if (HOST_NAME_SETTING.get(settings)) { - final String hostName = localNode != null ? localNode.getHostName() : null; - if (hostName != null) { - builder.append("[").append(hostName).append("] "); - } - } - if (NODE_NAME_SETTING.get(settings)) { - final String name = Node.NODE_NAME_SETTING.get(settings); - if (name != null) { - builder.append("[").append(name).append("] "); - } - } - return builder.toString(); + EntryCommonFields withNewSettings(Settings newSettings) { + final Settings mergedSettings = Settings.builder().put(this.settings).put(newSettings, false).build(); + return new EntryCommonFields(mergedSettings, this.localNode); } - private static String localOriginTag(@Nullable DiscoveryNode localNode) { - if (localNode == null) { - return "origin_type=[local_node]"; - } - return new StringBuilder("origin_type=[local_node], origin_address=[").append(localNode.getHostAddress()) - .append("]") - .toString(); + EntryCommonFields withNewLocalNode(DiscoveryNode newLocalNode) { + return new EntryCommonFields(this.settings, newLocalNode); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java index 56d5fec3f20..76482c5fa92 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/test/SecuritySettingsSource.java @@ -137,9 +137,10 @@ public class SecuritySettingsSource extends ClusterDiscoveryConfiguration.Unicas .put(XPackSettings.WATCHER_ENABLED.getKey(), false) .put(XPackSettings.MONITORING_ENABLED.getKey(), false) .put(XPackSettings.AUDIT_ENABLED.getKey(), randomBoolean()) - .put(LoggingAuditTrail.HOST_ADDRESS_SETTING.getKey(), randomBoolean()) - .put(LoggingAuditTrail.HOST_NAME_SETTING.getKey(), randomBoolean()) - .put(LoggingAuditTrail.NODE_NAME_SETTING.getKey(), randomBoolean()) + .put(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING.getKey(), randomBoolean()) + .put(LoggingAuditTrail.EMIT_HOST_NAME_SETTING.getKey(), randomBoolean()) + .put(LoggingAuditTrail.EMIT_NODE_NAME_SETTING.getKey(), randomBoolean()) + .put(LoggingAuditTrail.EMIT_NODE_ID_SETTING.getKey(), randomBoolean()) .put("xpack.security.authc.realms.file.type", FileRealmSettings.TYPE) .put("xpack.security.authc.realms.file.order", 0) .put("xpack.security.authc.realms.index.type", NativeRealmSettings.TYPE) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java index 2f32ba9c537..e05f4620ccc 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/AuditTrailSettingsUpdateTests.java @@ -21,11 +21,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; import java.util.List; -import java.util.regex.Pattern; - import static org.elasticsearch.test.ESIntegTestCase.Scope.TEST; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertAcked; import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -106,28 +106,45 @@ public class AuditTrailSettingsUpdateTests extends SecurityIntegTestCase { public void testDynamicHostSettings() { final boolean persistent = randomBoolean(); final Settings.Builder settingsBuilder = Settings.builder(); - settingsBuilder.put(LoggingAuditTrail.HOST_ADDRESS_SETTING.getKey(), true); - settingsBuilder.put(LoggingAuditTrail.HOST_NAME_SETTING.getKey(), true); - settingsBuilder.put(LoggingAuditTrail.NODE_NAME_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.EMIT_HOST_NAME_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.EMIT_NODE_NAME_SETTING.getKey(), true); + settingsBuilder.put(LoggingAuditTrail.EMIT_NODE_ID_SETTING.getKey(), true); updateSettings(settingsBuilder.build(), persistent); - final LoggingAuditTrail loggingAuditTrail = ((LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) + final LoggingAuditTrail loggingAuditTrail = (LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) .iterator() .next() .getAuditTrails() .iterator() - .next()); - assertTrue(Pattern.matches("\\[127\\.0\\.0\\.1\\] \\[127\\.0\\.0\\.1\\] \\[node_.*\\] ", loggingAuditTrail.localNodeInfo.prefix)); - settingsBuilder.put(LoggingAuditTrail.HOST_ADDRESS_SETTING.getKey(), false); + .next(); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_ID_FIELD_NAME), is(true)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is("127.0.0.1")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is("127.0.0.1")); + settingsBuilder.put(LoggingAuditTrail.EMIT_HOST_ADDRESS_SETTING.getKey(), false); updateSettings(settingsBuilder.build(), persistent); - assertTrue(Pattern.matches("\\[127\\.0\\.0\\.1\\] \\[node_.*\\] ", loggingAuditTrail.localNodeInfo.prefix)); - settingsBuilder.put(LoggingAuditTrail.HOST_ADDRESS_SETTING.getKey(), true); - settingsBuilder.put(LoggingAuditTrail.HOST_NAME_SETTING.getKey(), false); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_ID_FIELD_NAME), is(true)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is("127.0.0.1")); + settingsBuilder.put(LoggingAuditTrail.EMIT_HOST_NAME_SETTING.getKey(), false); updateSettings(settingsBuilder.build(), persistent); - assertTrue(Pattern.matches("\\[127\\.0\\.0\\.1\\] \\[node_.*\\] ", loggingAuditTrail.localNodeInfo.prefix)); - settingsBuilder.put(LoggingAuditTrail.HOST_NAME_SETTING.getKey(), true); - settingsBuilder.put(LoggingAuditTrail.NODE_NAME_SETTING.getKey(), false); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.get(LoggingAuditTrail.NODE_NAME_FIELD_NAME), startsWith("node_")); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_ID_FIELD_NAME), is(true)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); + settingsBuilder.put(LoggingAuditTrail.EMIT_NODE_NAME_SETTING.getKey(), false); updateSettings(settingsBuilder.build(), persistent); - assertTrue(Pattern.matches("\\[127\\.0\\.0\\.1\\] \\[127\\.0\\.0\\.1\\] ", loggingAuditTrail.localNodeInfo.prefix)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_NAME_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_ID_FIELD_NAME), is(true)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); + settingsBuilder.put(LoggingAuditTrail.EMIT_NODE_ID_SETTING.getKey(), false); + updateSettings(settingsBuilder.build(), persistent); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_NAME_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.NODE_ID_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_ADDRESS_FIELD_NAME), is(false)); + assertThat(loggingAuditTrail.entryCommonFields.commonFields.containsKey(LoggingAuditTrail.HOST_NAME_FIELD_NAME), is(false)); } public void testDynamicRequestBodySettings() { @@ -136,12 +153,12 @@ public class AuditTrailSettingsUpdateTests extends SecurityIntegTestCase { final Settings.Builder settingsBuilder = Settings.builder(); settingsBuilder.put(LoggingAuditTrail.INCLUDE_REQUEST_BODY.getKey(), enableRequestBody); updateSettings(settingsBuilder.build(), persistent); - final LoggingAuditTrail loggingAuditTrail = ((LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) + final LoggingAuditTrail loggingAuditTrail = (LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) .iterator() .next() .getAuditTrails() .iterator() - .next()); + .next(); assertEquals(enableRequestBody, loggingAuditTrail.includeRequestBody); settingsBuilder.put(LoggingAuditTrail.INCLUDE_REQUEST_BODY.getKey(), !enableRequestBody); updateSettings(settingsBuilder.build(), persistent); @@ -158,12 +175,12 @@ public class AuditTrailSettingsUpdateTests extends SecurityIntegTestCase { settingsBuilder.putList(LoggingAuditTrail.INCLUDE_EVENT_SETTINGS.getKey(), includedEvents); settingsBuilder.putList(LoggingAuditTrail.EXCLUDE_EVENT_SETTINGS.getKey(), excludedEvents); updateSettings(settingsBuilder.build(), randomBoolean()); - final LoggingAuditTrail loggingAuditTrail = ((LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) + final LoggingAuditTrail loggingAuditTrail = (LoggingAuditTrail) internalCluster().getInstances(AuditTrailService.class) .iterator() .next() .getAuditTrails() .iterator() - .next()); + .next(); assertEquals(AuditLevel.parse(includedEvents, excludedEvents), loggingAuditTrail.events); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java index 4c9df8fd9d3..67f38c0e581 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailFilterTests.java @@ -59,9 +59,6 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { private Settings settings; private DiscoveryNode localNode; private ClusterService clusterService; - private ThreadContext threadContext; - private Logger logger; - List logOutput; @Before public void init() throws Exception { @@ -83,12 +80,11 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { arg0.updateLocalNodeInfo(localNode); return null; }).when(clusterService).addListener(Mockito.isA(LoggingAuditTrail.class)); - threadContext = new ThreadContext(Settings.EMPTY); - logger = CapturingLogger.newCapturingLogger(Level.INFO); - logOutput = CapturingLogger.output(logger.getName(), Level.INFO); } public void testSingleCompletePolicyPredicate() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); // create complete filter policy final Settings.Builder settingsBuilder = Settings.builder().put(settings); // filter by username @@ -179,6 +175,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testSingleCompleteWithEmptyFieldPolicyPredicate() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); // create complete filter policy final Settings.Builder settingsBuilder = Settings.builder().put(settings); // filter by username @@ -275,6 +273,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testTwoPolicyPredicatesWithMissingFields() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final Settings.Builder settingsBuilder = Settings.builder().put(settings); // first policy: realms and roles filters final List filteredRealms = randomNonEmptyListOfFilteredNames(); @@ -341,6 +341,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testUsersFilter() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final List allFilteredUsers = new ArrayList<>(); final Settings.Builder settingsBuilder = Settings.builder().put(settings); for (int i = 0; i < randomIntBetween(1, 4); i++) { @@ -387,6 +389,7 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { final MockToken unfilteredToken = new MockToken(UNFILTER_MARKER + randomAlphaOfLengthBetween(1, 4)); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); + final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied auditTrail.anonymousAccessDenied("_action", message); if (filterMissingUser) { @@ -623,6 +626,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testRealmsFilter() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final List allFilteredRealms = new ArrayList<>(); final Settings.Builder settingsBuilder = Settings.builder().put(settings); for (int i = 0; i < randomIntBetween(1, 4); i++) { @@ -659,6 +664,7 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { final MockToken authToken = new MockToken("token1"); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); + final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied auditTrail.anonymousAccessDenied("_action", message); if (filterMissingRealm) { @@ -908,6 +914,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testRolesFilter() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final List> allFilteredRoles = new ArrayList<>(); final Settings.Builder settingsBuilder = Settings.builder().put(settings); for (int i = 0; i < randomIntBetween(1, 4); i++) { @@ -966,6 +974,7 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { final MockToken authToken = new MockToken("token1"); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); + final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied auditTrail.anonymousAccessDenied("_action", message); if (filterMissingRoles) { @@ -1179,6 +1188,8 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { } public void testIndicesFilter() throws Exception { + final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); final List> allFilteredIndices = new ArrayList<>(); final Settings.Builder settingsBuilder = Settings.builder().put(settings); for (int i = 0; i < randomIntBetween(1, 3); i++) { @@ -1236,6 +1247,7 @@ public class LoggingAuditTrailFilterTests extends ESTestCase { final TransportMessage noIndexMessage = new MockMessage(threadContext); final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settingsBuilder.build(), clusterService, logger, threadContext); + final List logOutput = CapturingLogger.output(logger.getName(), Level.INFO); // anonymous accessDenied auditTrail.anonymousAccessDenied("_action", noIndexMessage); if (filterMissingIndices) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java index 1059e22abd6..6c302c9311f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/audit/logfile/LoggingAuditTrailTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.audit.logfile; import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.core.layout.PatternLayout; import org.elasticsearch.action.IndicesRequest; import org.elasticsearch.action.support.IndicesOptions; import org.elasticsearch.cluster.node.DiscoveryNode; @@ -14,6 +15,7 @@ import org.elasticsearch.cluster.service.ClusterService; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.MapBuilder; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.network.NetworkAddress; import org.elasticsearch.common.settings.ClusterSettings; @@ -39,23 +41,29 @@ import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.rest.RemoteHostHeader; import org.elasticsearch.xpack.security.transport.filter.IPFilter; import org.elasticsearch.xpack.security.transport.filter.SecurityIpFilterRule; +import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; +import org.junit.BeforeClass; import org.mockito.stubbing.Answer; import java.io.IOException; +import java.io.InputStream; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; - -import static org.hamcrest.Matchers.equalTo; +import java.util.Properties; +import java.util.regex.Pattern; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.containsString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -113,17 +121,46 @@ public class LoggingAuditTrailTests extends ESTestCase { }; protected abstract boolean hasContent(); + protected abstract BytesReference content(); + protected abstract String expectedMessage(); } - private String prefix; + private static PatternLayout patternLayout; private Settings settings; private DiscoveryNode localNode; private ClusterService clusterService; private ThreadContext threadContext; private boolean includeRequestBody; - private String opaqueId; + private Map commonFields; + private Logger logger; + private LoggingAuditTrail auditTrail; + + @BeforeClass + public static void lookupPatternLayout() throws Exception { + final Properties properties = new Properties(); + try (InputStream configStream = LoggingAuditTrail.class.getClassLoader().getResourceAsStream("log4j2.properties")) { + properties.load(configStream); + } + // This is a minimal and brittle parsing of the security log4j2 config + // properties. If any of these fails, then surely the config file changed. In + // this case adjust the assertions! The goal of this assertion chain is to + // validate that the layout pattern we are testing with is indeed the one + // attached to the LoggingAuditTrail.class logger. + assertThat(properties.getProperty("logger.xpack_security_audit_logfile.name"), is(LoggingAuditTrail.class.getName())); + assertThat(properties.getProperty("logger.xpack_security_audit_logfile.appenderRef.audit_rolling.ref"), is("audit_rolling")); + assertThat(properties.getProperty("appender.audit_rolling.name"), is("audit_rolling")); + assertThat(properties.getProperty("appender.audit_rolling.layout.type"), is("PatternLayout")); + final String patternLayoutFormat = properties.getProperty("appender.audit_rolling.layout.pattern"); + assertThat(patternLayoutFormat, is(notNullValue())); + patternLayout = PatternLayout.newBuilder().withPattern(patternLayoutFormat).withCharset(StandardCharsets.UTF_8).build(); + } + + @AfterClass + public static void releasePatternLayout() { + patternLayout = null; + } @Before public void init() throws Exception { @@ -135,7 +172,7 @@ public class LoggingAuditTrailTests extends ESTestCase { .put("xpack.security.audit.logfile.events.emit_request_body", includeRequestBody) .build(); localNode = mock(DiscoveryNode.class); - when(localNode.getHostAddress()).thenReturn(buildNewFakeTransportAddress().toString()); + when(localNode.getAddress()).thenReturn(buildNewFakeTransportAddress()); clusterService = mock(ClusterService.class); when(clusterService.localNode()).thenReturn(localNode); Mockito.doAnswer((Answer) invocation -> { @@ -145,597 +182,708 @@ public class LoggingAuditTrailTests extends ESTestCase { }).when(clusterService).addListener(Mockito.isA(LoggingAuditTrail.class)); final ClusterSettings clusterSettings = mockClusterSettings(); when(clusterService.getClusterSettings()).thenReturn(clusterSettings); - prefix = LoggingAuditTrail.LocalNodeInfo.resolvePrefix(settings, localNode); + commonFields = new LoggingAuditTrail.EntryCommonFields(settings, localNode).commonFields; threadContext = new ThreadContext(Settings.EMPTY); if (randomBoolean()) { - String id = randomAlphaOfLength(10); - threadContext.putHeader(Task.X_OPAQUE_ID, id); - opaqueId = ", opaque_id=[" + id + "]"; - } else { - opaqueId = ""; + threadContext.putHeader(Task.X_OPAQUE_ID, randomAlphaOfLengthBetween(1, 4)); } + logger = CapturingLogger.newCapturingLogger(Level.INFO, patternLayout); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + } + + @After + public void clearLog() throws Exception { + CapturingLogger.output(logger.getName(), Level.INFO).clear(); } public void testAnonymousAccessDeniedTransport() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); + auditTrail.anonymousAccessDenied("_action", message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [anonymous_access_denied]\t" + origins + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [anonymous_access_denied]\t" + origins + - ", action=[_action], request=[MockMessage]" + opaqueId); - } + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action"); + indicesRequest(message, checkedFields, checkedArrayFields); + restOrTransportOrigin(message, threadContext, checkedFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.anonymousAccessDenied("_action", message); assertEmptyLog(logger); } public void testAnonymousAccessDeniedRest() throws Exception { - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200)); + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.anonymousAccessDenied(request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, prefix + "[rest] [anonymous_access_denied]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, prefix + "[rest] [anonymous_access_denied]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId); - } + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "anonymous_access_denied") + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, null); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "anonymous_access_denied") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.anonymousAccessDenied(request); assertEmptyLog(logger); } public void testAuthenticationFailed() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + final AuthenticationToken mockToken = new MockToken(); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.authenticationFailed(new MockToken(), "_action", message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + - ", principal=[_principal], action=[_action], indices=[" + indices(message) + - "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + - ", principal=[_principal], action=[_action], request=[MockMessage]" + opaqueId); - } + + auditTrail.authenticationFailed(mockToken, "_action", message); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.authenticationFailed(new MockToken(), "_action", message); assertEmptyLog(logger); } public void testAuthenticationFailedNoToken() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); + auditTrail.authenticationFailed("_action", message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_failed]\t" + origins + - ", action=[_action], request=[MockMessage]" + opaqueId); - } + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.authenticationFailed("_action", message); assertEmptyLog(logger); } public void testAuthenticationFailedRest() throws Exception { - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200)); + final Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("foo", "bar"); + } + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address, params); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed(new MockToken(), request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + - NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId + ", request_body=[" + - expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + - NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId); - } + final AuthenticationToken mockToken = new MockToken(); + + auditTrail.authenticationFailed(mockToken, request); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, null) + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "foo=bar"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.authenticationFailed(new MockToken(), request); assertEmptyLog(logger); } public void testAuthenticationFailedRestNoToken() throws Exception { - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200)); + final Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("bar", "baz"); + } + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address, params); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.authenticationFailed(request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, prefix + "[rest] [authentication_failed]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId); - } + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_failed") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, null) + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, null) + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "bar=baz"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.authenticationFailed(request); assertEmptyLog(logger); } public void testAuthenticationFailedRealm() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + final AuthenticationToken mockToken = new MockToken(); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), "_action", message); + final String realm = randomAlphaOfLengthBetween(1, 6); + auditTrail.authenticationFailed(realm, mockToken, "_action", message); assertEmptyLog(logger); // test enabled - settings = - Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "realm_authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.include", "realm_authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.authenticationFailed("_realm", new MockToken(), "_action", message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [realm_authentication_failed]\trealm=[_realm], " + origins + - ", principal=[_principal], action=[_action], indices=[" + indices(message) + "], " + - "request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [realm_authentication_failed]\trealm=[_realm], " + origins + - ", principal=[_principal], action=[_action], request=[MockMessage]" + opaqueId); - } + auditTrail.authenticationFailed(realm, mockToken, "_action", message); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") + .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); } public void testAuthenticationFailedRealmRest() throws Exception { - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200)); + final Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("_param", "baz"); + } + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address, params); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), request); + final AuthenticationToken mockToken = new MockToken(); + final String realm = randomAlphaOfLengthBetween(1, 6); + auditTrail.authenticationFailed(realm, mockToken, request); assertEmptyLog(logger); // test enabled - settings = - Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "realm_authentication_failed").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.include", "realm_authentication_failed") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationFailed("_realm", new MockToken(), request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, prefix + "[rest] [realm_authentication_failed]\trealm=[_realm], origin_address=[" + - NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId + ", request_body=[" + - expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, prefix + "[rest] [realm_authentication_failed]\trealm=[_realm], origin_address=[" + - NetworkAddress.format(address) + "], principal=[_principal], uri=[_uri]" + opaqueId); - } + auditTrail.authenticationFailed(realm, mockToken, request); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "realm_authentication_failed") + .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, mockToken.principal()) + .put(LoggingAuditTrail.ACTION_FIELD_NAME, null) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "_param=baz"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); } public void testAccessGranted() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[]{"r1"}); - } - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); - final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" - : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", " + userInfo + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", " + userInfo + - ", action=[_action], request=[MockMessage]" + opaqueId); - } + final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = createAuthentication(); + + auditTrail.accessGranted(authentication, "_action", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + subject(authentication, checkedFields); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_granted").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "access_granted") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessGranted(authentication, "_action", message, roles); assertEmptyLog(logger); } public void testAccessGrantedInternalSystemAction() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); + final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = new Authentication(SystemUser.INSTANCE, new RealmRef("_reserved", "test", "foo"), null); + auditTrail.accessGranted(authentication, "internal:_action", message, roles); assertEmptyLog(logger); // test enabled - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "system_access_granted").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.include", "system_access_granted") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.accessGranted(createAuthentication(SystemUser.INSTANCE), "internal:_action", message, new String[] { role }); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", principal=[" + - SystemUser.INSTANCE.principal() - + "], realm=[authRealm], roles=[" + role + "], action=[internal:_action], indices=[" + indices(message) - + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", principal=[" + - SystemUser.INSTANCE.principal() + "], realm=[authRealm], roles=[" + role - + "], action=[internal:_action], request=[MockMessage]" + opaqueId); - } + auditTrail.accessGranted(authentication, "internal:_action", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, SystemUser.INSTANCE.principal()) + .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, "_reserved") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); } public void testAccessGrantedInternalSystemActionNonSystemUser() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[]{"r1"}); - } - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessGranted(createAuthentication(user), "internal:_action", message, new String[] { role }); - final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" - : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", " + userInfo + - ", action=[internal:_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_granted]\t" + origins + ", " + userInfo + - ", action=[internal:_action], request=[MockMessage]" + opaqueId); - } + final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = createAuthentication(); + + auditTrail.accessGranted(authentication, "internal:_action", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_granted") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "internal:_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + subject(authentication, checkedFields); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_granted").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "access_granted") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessGranted(createAuthentication(user), "internal:_action", message, new String[] { role }); + auditTrail.accessGranted(authentication, "internal:_action", message, roles); assertEmptyLog(logger); } public void testAccessDenied() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[]{"r1"}); - } - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); - final String userInfo = (runAs ? "principal=[running as], realm=[lookRealm], run_by_principal=[_username], run_by_realm=[authRealm]" - : "principal=[_username], realm=[authRealm]") + ", roles=[" + role + "]"; - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_denied]\t" + origins + ", " + userInfo + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [access_denied]\t" + origins + ", " + userInfo + - ", action=[_action], request=[MockMessage]" + opaqueId); - } + final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = createAuthentication(); + + auditTrail.accessDenied(authentication, "_action/bar", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "access_denied") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action/bar") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + subject(authentication, checkedFields); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "access_denied").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "access_denied") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + auditTrail.accessDenied(authentication, "_action", message, roles); assertEmptyLog(logger); } public void testTamperedRequestRest() throws Exception { - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200)); + final Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("_param", "baz"); + } + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address, params); final String expectedMessage = tuple.v1().expectedMessage(); final RestRequest request = tuple.v2(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.tamperedRequest(request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, prefix + "[rest] [tampered_request]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId + ", request_body=[" + expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, prefix + "[rest] [tampered_request]\torigin_address=[" + - NetworkAddress.format(address) + "], uri=[_uri]" + opaqueId); - } + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "_param=baz"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "tampered_request").build(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "tampered_request") + .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); auditTrail.tamperedRequest(request); assertEmptyLog(logger); } public void testTamperedRequest() throws Exception { - final String action = "_action"; final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.tamperedRequest(action, message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + - ", action=[_action], request=[MockMessage]" + opaqueId); - } - // test disabled - - } - - public void testTamperedRequestWithUser() throws Exception { - final String action = "_action"; - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[]{"r1"}); - } - final String userInfo = runAs ? "principal=[running as], run_by_principal=[_username]" : "principal=[_username]"; - final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - auditTrail.tamperedRequest(user, action, message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + ", " + userInfo + - ", action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [tampered_request]\t" + origins + ", " + userInfo + - ", action=[_action], request=[MockMessage]" + opaqueId); - } - - // test disabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "tampered_request").build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.tamperedRequest(user, action, message); - assertEmptyLog(logger); - } - - public void testConnectionDenied() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final InetAddress inetAddress = InetAddress.getLoopbackAddress(); - final SecurityIpFilterRule rule = new SecurityIpFilterRule(false, "_all"); - auditTrail.connectionDenied(inetAddress, "default", rule); - assertMsg(logger, Level.INFO, String.format(Locale.ROOT, prefix + - "[ip_filter] [connection_denied]\torigin_address=[%s], transport_profile=[%s], rule=[deny %s]" + opaqueId, - NetworkAddress.format(inetAddress), "default", "_all")); - - // test disabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "connection_denied").build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.connectionDenied(inetAddress, "default", rule); - assertEmptyLog(logger); - } - - public void testConnectionGranted() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final InetAddress inetAddress = InetAddress.getLoopbackAddress(); - final SecurityIpFilterRule rule = IPFilter.DEFAULT_PROFILE_ACCEPT_ALL; - auditTrail.connectionGranted(inetAddress, "default", rule); - assertEmptyLog(logger); - - // test enabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.include", "connection_granted").build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.connectionGranted(inetAddress, "default", rule); - assertMsg(logger, Level.INFO, String.format(Locale.ROOT, prefix + "[ip_filter] [connection_granted]\torigin_address=[%s], " + - "transport_profile=[default], rule=[allow default:accept_all]" + opaqueId, - NetworkAddress.format(inetAddress))); - } - - public void testRunAsGranted() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, - prefix + "[transport] [run_as_granted]\t" + origins - + ", principal=[_username], realm=[authRealm], run_as_principal=[running as], run_as_realm=[lookRealm], roles=[" - + role + "], action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, - prefix + "[transport] [run_as_granted]\t" + origins - + ", principal=[_username], realm=[authRealm], run_as_principal=[running as], run_as_realm=[lookRealm], roles=[" - + role + "], action=[_action], request=[MockMessage]" + opaqueId); - } - - // test disabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "run_as_granted").build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); - assertEmptyLog(logger); - } - - public void testRunAsDenied() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final User user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - final String role = randomAlphaOfLengthBetween(1, 6); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, - prefix + "[transport] [run_as_denied]\t" + origins - + ", principal=[_username], realm=[authRealm], run_as_principal=[running as], run_as_realm=[lookRealm], roles=[" - + role + "], action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, - prefix + "[transport] [run_as_denied]\t" + origins - + ", principal=[_username], realm=[authRealm], run_as_principal=[running as], run_as_realm=[lookRealm], roles=[" - + role + "], action=[_action], request=[MockMessage]" + opaqueId); - } - - // test disabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(settings).put("xpack.security.audit.logfile.events.exclude", "run_as_denied").build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); - assertEmptyLog(logger); - } - - public void testOriginAttributes() throws Exception { - final MockMessage message = new MockMessage(threadContext); - final LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final String text = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final InetSocketAddress restAddress = RemoteHostHeader.restRemoteAddress(threadContext); - if (restAddress != null) { - assertThat(text, equalTo("origin_type=[rest], origin_address=[" + - NetworkAddress.format(restAddress.getAddress()) + "]")); - return; - } - final TransportAddress address = message.remoteAddress(); - if (address == null) { - assertThat(text, equalTo("origin_type=[local_node], origin_address=[" + localNode.getHostAddress() + "]")); - return; - } - - assertThat(text, equalTo("origin_type=[transport], origin_address=[" + - NetworkAddress.format(address.address().getAddress()) + "]")); - } - - public void testAuthenticationSuccessRest() throws Exception { - final Map params = new HashMap<>(); - params.put("foo", "bar"); - final InetAddress address = forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"); - final Tuple tuple = prepareRestContent("_uri", new InetSocketAddress(address, 9200), params); - final String expectedMessage = tuple.v1().expectedMessage(); - final RestRequest request = tuple.v2(); - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[] { "r1" }); - } - final String userInfo = runAs ? "principal=[running as], run_by_principal=[_username]" : "principal=[_username]"; - final String realm = "_realm"; - - Settings settings = Settings.builder().put(this.settings) - .put("xpack.security.audit.logfile.events.include", "authentication_success") - .build(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, request); - if (includeRequestBody) { - assertMsg(logger, Level.INFO, - prefix + "[rest] [authentication_success]\t" + userInfo + ", realm=[_realm], uri=[_uri], params=[" + params - + "]" + opaqueId + ", request_body=[" + expectedMessage + "]"); - } else { - assertMsg(logger, Level.INFO, - prefix + "[rest] [authentication_success]\t" + userInfo + ", realm=[_realm], uri=[_uri], params=[" + params - + "]" + opaqueId); - } - - // test disabled - CapturingLogger.output(logger.getName(), Level.INFO).clear(); - settings = Settings.builder().put(this.settings).put("xpack.security.audit.logfile.events.exclude", "authentication_success") - .build(); - auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, request); - assertEmptyLog(logger); - } - - public void testAuthenticationSuccessTransport() throws Exception { - Settings settings = Settings.builder().put(this.settings) - .put("xpack.security.audit.logfile.events.include", "authentication_success").build(); - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - LoggingAuditTrail auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); - final String origins = LoggingAuditTrail.originAttributes(threadContext, message, auditTrail.localNodeInfo); - final boolean runAs = randomBoolean(); - User user; - if (runAs) { - user = new User("running as", new String[]{"r2"}, new User("_username", new String[] {"r1"})); - } else { - user = new User("_username", new String[] { "r1" }); - } - final String userInfo = runAs ? "principal=[running as], run_by_principal=[_username]" : "principal=[_username]"; - final String realm = "_realm"; - auditTrail.authenticationSuccess(realm, user, "_action", message); - if (message instanceof IndicesRequest) { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_success]\t" + origins + ", " + userInfo - + ", realm=[_realm], action=[_action], indices=[" + indices(message) + "], request=[MockIndicesRequest]" + opaqueId); - } else { - assertMsg(logger, Level.INFO, prefix + "[transport] [authentication_success]\t" + origins + ", " + userInfo - + ", realm=[_realm], action=[_action], request=[MockMessage]" + opaqueId); - } + auditTrail.tamperedRequest("_action", message); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); // test disabled CapturingLogger.output(logger.getName(), Level.INFO).clear(); settings = Settings.builder() - .put(this.settings) - .put("xpack.security.audit.logfile.events.exclude", "authentication_success") + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "tampered_request") .build(); auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); - auditTrail.authenticationSuccess(realm, user, "_action", message); + auditTrail.tamperedRequest("_action", message); assertEmptyLog(logger); } + public void testTamperedRequestWithUser() throws Exception { + final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); + final boolean runAs = randomBoolean(); + final User user; + if (runAs) { + user = new User("running_as", new String[] { "r2" }, new User("_username", new String[] { "r1" })); + } else { + user = new User("_username", new String[] { "r1" }); + } + + auditTrail.tamperedRequest(user, "_action", message); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "tampered_request") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + if (runAs) { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "running_as"); + checkedFields.put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, "_username"); + } else { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "_username"); + } + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); + + // test disabled + CapturingLogger.output(logger.getName(), Level.INFO).clear(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "tampered_request") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.tamperedRequest(user, "_action", message); + assertEmptyLog(logger); + } + + public void testConnectionDenied() throws Exception { + final InetAddress inetAddress = InetAddress.getLoopbackAddress(); + final SecurityIpFilterRule rule = new SecurityIpFilterRule(false, "_all"); + final String profile = randomAlphaOfLengthBetween(1, 6); + + auditTrail.connectionDenied(inetAddress, profile, rule); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.IP_FILTER_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "connection_denied") + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.IP_FILTER_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(inetAddress)) + .put(LoggingAuditTrail.TRANSPORT_PROFILE_FIELD_NAME, profile) + .put(LoggingAuditTrail.RULE_FIELD_NAME, "deny _all"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); + + // test disabled + CapturingLogger.output(logger.getName(), Level.INFO).clear(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "connection_denied") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.connectionDenied(inetAddress, profile, rule); + assertEmptyLog(logger); + } + + public void testConnectionGranted() throws Exception { + final InetAddress inetAddress = InetAddress.getLoopbackAddress(); + final SecurityIpFilterRule rule = IPFilter.DEFAULT_PROFILE_ACCEPT_ALL; + final String profile = randomAlphaOfLengthBetween(1, 6); + + auditTrail.connectionGranted(inetAddress, profile, rule); + assertEmptyLog(logger); + + // test enabled + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.include", "connection_granted") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.connectionGranted(inetAddress, profile, rule); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.IP_FILTER_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "connection_granted") + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.IP_FILTER_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(inetAddress)) + .put(LoggingAuditTrail.TRANSPORT_PROFILE_FIELD_NAME, profile) + .put(LoggingAuditTrail.RULE_FIELD_NAME, "allow default:accept_all"); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); + } + + public void testRunAsGranted() throws Exception { + final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); + final String[] roles = randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = new Authentication( + new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), + new RealmRef("authRealm", "test", "foo"), + new RealmRef("lookRealm", "up", "by")); + + auditTrail.runAsGranted(authentication, "_action", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "run_as_granted") + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "_username") + .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, "authRealm") + .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_FIELD_NAME, "running as") + .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_REALM_FIELD_NAME, "lookRealm") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); + + // test disabled + CapturingLogger.output(logger.getName(), Level.INFO).clear(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "run_as_granted") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.runAsGranted(authentication, "_action", message, roles); + assertEmptyLog(logger); + } + + public void testRunAsDenied() throws Exception { + final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); + final String[] roles = randomArray(0, 4, String[]::new, () -> randomAlphaOfLengthBetween(1, 4)); + final Authentication authentication = new Authentication( + new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })), + new RealmRef("authRealm", "test", "foo"), + new RealmRef("lookRealm", "up", "by")); + + auditTrail.runAsDenied(authentication, "_action", message, roles); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "run_as_denied") + .put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "_username") + .put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, "authRealm") + .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_FIELD_NAME, "running as") + .put(LoggingAuditTrail.PRINCIPAL_RUN_AS_REALM_FIELD_NAME, "lookRealm") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.PRINCIPAL_ROLES_FIELD_NAME, roles); + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); + + // test disabled + CapturingLogger.output(logger.getName(), Level.INFO).clear(); + settings = Settings.builder() + .put(settings) + .put("xpack.security.audit.logfile.events.exclude", "run_as_denied") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.runAsDenied(authentication, "_action", message, roles); + assertEmptyLog(logger); + } + + public void testAuthenticationSuccessRest() throws Exception { + final Map params = new HashMap<>(); + if (randomBoolean()) { + params.put("foo", "bar"); + params.put("evac", "true"); + } + final InetSocketAddress address = new InetSocketAddress(forge("_hostname", randomBoolean() ? "127.0.0.1" : "::1"), + randomIntBetween(9200, 9300)); + final Tuple tuple = prepareRestContent("_uri", address, params); + final String expectedMessage = tuple.v1().expectedMessage(); + final RestRequest request = tuple.v2(); + final String realm = randomAlphaOfLengthBetween(1, 6); + final User user; + if (randomBoolean()) { + user = new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })); + } else { + user = new User("_username", new String[] { "r1" }); + } + + // event by default disabled + auditTrail.authenticationSuccess(realm, user, request); + assertEmptyLog(logger); + + settings = Settings.builder() + .put(this.settings) + .put("xpack.security.audit.logfile.events.include", "authentication_success") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.authenticationSuccess(realm, user, request); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_success") + .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) + .put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address)) + .put(LoggingAuditTrail.REQUEST_BODY_FIELD_NAME, + includeRequestBody && Strings.hasLength(expectedMessage) ? expectedMessage : null) + .put(LoggingAuditTrail.URL_PATH_FIELD_NAME, "_uri") + .put(LoggingAuditTrail.URL_QUERY_FIELD_NAME, params.isEmpty() ? null : "foo=bar&evac=true"); + if (user.isRunAs()) { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "running as"); + checkedFields.put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, "_username"); + } else { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "_username"); + } + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap()); + } + + public void testAuthenticationSuccessTransport() throws Exception { + final TransportMessage message = randomBoolean() ? new MockMessage(threadContext) : new MockIndicesRequest(threadContext); + final User user; + if (randomBoolean()) { + user = new User("running as", new String[] { "r2" }, new User("_username", new String[] { "r1" })); + } else { + user = new User("_username", new String[] { "r1" }); + } + final String realm = randomAlphaOfLengthBetween(1, 6); + + // event by default disabled + auditTrail.authenticationSuccess(realm, user, "_action", message); + assertEmptyLog(logger); + + settings = Settings.builder() + .put(this.settings) + .put("xpack.security.audit.logfile.events.include", "authentication_success") + .build(); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); + auditTrail.authenticationSuccess(realm, user, "_action", message); + final MapBuilder checkedFields = new MapBuilder<>(commonFields); + final MapBuilder checkedArrayFields = new MapBuilder<>(); + checkedFields.put(LoggingAuditTrail.EVENT_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.EVENT_ACTION_FIELD_NAME, "authentication_success") + .put(LoggingAuditTrail.ACTION_FIELD_NAME, "_action") + .put(LoggingAuditTrail.REALM_FIELD_NAME, realm) + .put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, message.getClass().getSimpleName()); + if (user.isRunAs()) { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "running as"); + checkedFields.put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, "_username"); + } else { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, "_username"); + } + restOrTransportOrigin(message, threadContext, checkedFields); + indicesRequest(message, checkedFields, checkedArrayFields); + opaqueId(threadContext, checkedFields); + assertMsg(logger, checkedFields.immutableMap(), checkedArrayFields.immutableMap()); + } + public void testRequestsWithoutIndices() throws Exception { - final Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - final Settings allEventsSettings = Settings.builder() + settings = Settings.builder() .put(settings) .put("xpack.security.audit.logfile.events.include", "_all") .build(); - final LoggingAuditTrail auditTrail = new LoggingAuditTrail(allEventsSettings, clusterService, logger, threadContext); + auditTrail = new LoggingAuditTrail(settings, clusterService, logger, threadContext); final User user = new User("_username", new String[] { "r1" }); final String role = randomAlphaOfLengthBetween(1, 6); final String realm = randomAlphaOfLengthBetween(1, 6); @@ -748,48 +896,93 @@ public class LoggingAuditTrailTests extends ESTestCase { for (final TransportMessage message : messages) { auditTrail.anonymousAccessDenied("_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.authenticationFailed(new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.authenticationFailed("_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.authenticationFailed(realm, new MockToken(), "_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.accessGranted(createAuthentication(user), "_action", message, new String[] { role }); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); + auditTrail.accessGranted(createAuthentication(), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.accessDenied(createAuthentication(user), "_action", message, new String[] { role }); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); + auditTrail.accessDenied(createAuthentication(), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.tamperedRequest("_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.tamperedRequest(user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.runAsGranted(createAuthentication(user), "_action", message, new String[] { role }); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); + auditTrail.runAsGranted(createAuthentication(), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); - auditTrail.runAsDenied(createAuthentication(user), "_action", message, new String[] { role }); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); + auditTrail.runAsDenied(createAuthentication(), "_action", message, new String[] { role }); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); auditTrail.authenticationSuccess(realm, user, "_action", message); assertThat(output.size(), is(logEntriesCount++)); - assertThat(output.get(logEntriesCount - 2), not(containsString("indices=["))); + assertThat(output.get(logEntriesCount - 2), not(containsString("indices="))); } } - private void assertMsg(Logger logger, Level level, String message) { - final List output = CapturingLogger.output(logger.getName(), level); - assertThat(output.size(), is(1)); - assertThat(output.get(0), equalTo(message)); + private void assertMsg(Logger logger, Map checkFields) { + assertMsg(logger, checkFields, Collections.emptyMap()); + } + + private void assertMsg(Logger logger, Map checkFields, Map checkArrayFields) { + final List output = CapturingLogger.output(logger.getName(), Level.INFO); + assertThat("Exactly one logEntry expected. Found: " + output.size(), output.size(), is(1)); + if (checkFields == null) { + // only check msg existence + return; + } + String logLine = output.get(0); + // check each field + for (final Map.Entry checkField : checkFields.entrySet()) { + if (null == checkField.getValue()) { + // null checkField means that the field does not exist + assertThat("Field: " + checkField.getKey() + " should be missing.", + logLine.contains(Pattern.quote("\"" + checkField.getKey() + "\":")), is(false)); + } else { + final String quotedValue = "\"" + checkField.getValue().replaceAll("\"", "\\\\\"") + "\""; + final Pattern logEntryFieldPattern = Pattern.compile(Pattern.quote("\"" + checkField.getKey() + "\":" + quotedValue)); + assertThat("Field " + checkField.getKey() + " value mismatch. Expected " + quotedValue, + logEntryFieldPattern.matcher(logLine).find(), is(true)); + // remove checked field + logLine = logEntryFieldPattern.matcher(logLine).replaceFirst(""); + } + } + for (final Map.Entry checkArrayField : checkArrayFields.entrySet()) { + if (null == checkArrayField.getValue()) { + // null checkField means that the field does not exist + assertThat("Field: " + checkArrayField.getKey() + " should be missing.", + logLine.contains(Pattern.quote("\"" + checkArrayField.getKey() + "\":")), is(false)); + } else { + final String quotedValue = "[" + Arrays.asList(checkArrayField.getValue()) + .stream() + .filter(s -> s != null) + .map(s -> "\"" + s.replaceAll("\"", "\\\\\"") + "\"") + .reduce((x, y) -> x + "," + y) + .orElse("") + "]"; + final Pattern logEntryFieldPattern = Pattern.compile(Pattern.quote("\"" + checkArrayField.getKey() + "\":" + quotedValue)); + assertThat("Field " + checkArrayField.getKey() + " value mismatch. Expected " + quotedValue, + logEntryFieldPattern.matcher(logLine).find(), is(true)); + // remove checked field + logLine = logEntryFieldPattern.matcher(logLine).replaceFirst(""); + } + } + logLine = logLine.replaceFirst("\"@timestamp\":\"[^\"]*\"", "").replaceAll("[{},]", ""); + // check no extra fields + assertThat("Log event has extra unexpected content: " + logLine, Strings.hasText(logLine), is(false)); } private void assertEmptyLog(Logger logger) { - assertThat(CapturingLogger.isEmpty(logger.getName()), is(true)); + assertThat("Logger is not empty", CapturingLogger.isEmpty(logger.getName()), is(true)); } protected Tuple prepareRestContent(String uri, InetSocketAddress remoteAddress) { @@ -802,7 +995,18 @@ public class LoggingAuditTrailTests extends ESTestCase { if (content.hasContent()) { builder.withContent(content.content(), XContentType.JSON); } - builder.withPath(uri); + if (params.isEmpty()) { + builder.withPath(uri); + } else { + final StringBuilder queryString = new StringBuilder("?"); + for (final Map.Entry entry : params.entrySet()) { + if (queryString.length() > 1) { + queryString.append('&'); + } + queryString.append(entry.getKey() + "=" + entry.getValue()); + } + builder.withPath(uri + queryString.toString()); + } builder.withRemoteAddress(remoteAddress); builder.withParams(params); return new Tuple<>(content, builder.build()); @@ -814,12 +1018,16 @@ public class LoggingAuditTrailTests extends ESTestCase { return InetAddress.getByAddress(hostname, bytes); } - private static String indices(TransportMessage message) { - return Strings.arrayToCommaDelimitedString(((IndicesRequest) message).indices()); - } - - private static Authentication createAuthentication(User user) { - final RealmRef lookedUpBy = user.authenticatedUser() == user ? null : new RealmRef("lookRealm", "up", "by"); + private static Authentication createAuthentication() { + final RealmRef lookedUpBy; + final User user; + if (randomBoolean()) { + user = new User("running_as", new String[] { "r2" }, new User("_username", new String[] { "r1" })); + lookedUpBy = new RealmRef("lookRealm", "up", "by"); + } else { + user = new User("_username", new String[] { "r1" }); + lookedUpBy = null; + } return new Authentication(user, new RealmRef("authRealm", "test", "foo"), lookedUpBy); } @@ -849,7 +1057,8 @@ public class LoggingAuditTrailTests extends ESTestCase { static class MockIndicesRequest extends org.elasticsearch.action.MockIndicesRequest { MockIndicesRequest(ThreadContext threadContext) throws IOException { - super(IndicesOptions.strictExpandOpenAndForbidClosed(), "idx1", "idx2"); + super(IndicesOptions.strictExpandOpenAndForbidClosed(), + randomArray(0, 4, String[]::new, () -> randomBoolean() ? null : randomAlphaOfLengthBetween(1, 4))); if (randomBoolean()) { remoteAddress(buildNewFakeTransportAddress()); } @@ -882,4 +1091,50 @@ public class LoggingAuditTrailTests extends ESTestCase { } } + private static void restOrTransportOrigin(TransportMessage message, ThreadContext threadContext, + MapBuilder checkedFields) { + final InetSocketAddress restAddress = RemoteHostHeader.restRemoteAddress(threadContext); + if (restAddress != null) { + checkedFields.put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.REST_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(restAddress)); + } else { + final TransportAddress address = message.remoteAddress(); + if (address != null) { + checkedFields.put(LoggingAuditTrail.ORIGIN_TYPE_FIELD_NAME, LoggingAuditTrail.TRANSPORT_ORIGIN_FIELD_VALUE) + .put(LoggingAuditTrail.ORIGIN_ADDRESS_FIELD_NAME, NetworkAddress.format(address.address())); + } + } + } + + private static void subject(Authentication authentication, MapBuilder checkedFields) { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_FIELD_NAME, authentication.getUser().principal()); + if (authentication.getUser().isRunAs()) { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, authentication.getLookedUpBy().getName()) + .put(LoggingAuditTrail.PRINCIPAL_RUN_BY_FIELD_NAME, authentication.getUser().authenticatedUser().principal()) + .put(LoggingAuditTrail.PRINCIPAL_RUN_BY_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName()); + } else { + checkedFields.put(LoggingAuditTrail.PRINCIPAL_REALM_FIELD_NAME, authentication.getAuthenticatedBy().getName()); + } + } + + private static void opaqueId(ThreadContext threadContext, MapBuilder checkedFields) { + final String opaqueId = threadContext.getHeader(Task.X_OPAQUE_ID); + if (opaqueId != null) { + checkedFields.put(LoggingAuditTrail.OPAQUE_ID_FIELD_NAME, opaqueId); + } else { + checkedFields.put(LoggingAuditTrail.OPAQUE_ID_FIELD_NAME, null); + } + } + + private static void indicesRequest(TransportMessage message, MapBuilder checkedFields, + MapBuilder checkedArrayFields) { + if (message instanceof IndicesRequest) { + checkedFields.put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, MockIndicesRequest.class.getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.INDICES_FIELD_NAME, ((IndicesRequest) message).indices()); + } else { + checkedFields.put(LoggingAuditTrail.REQUEST_NAME_FIELD_NAME, MockMessage.class.getSimpleName()); + checkedArrayFields.put(LoggingAuditTrail.INDICES_FIELD_NAME, null); + } + } + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java index 739952af63e..3acb4888d78 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserPasswdStoreTests.java @@ -185,7 +185,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile_Empty() throws Exception { Path empty = createTempFile(); - Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); Map users = FileUserPasswdStore.parseFile(empty, logger, Settings.EMPTY); assertThat(users.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.DEBUG); @@ -195,7 +195,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { public void testParseFile_WhenFileDoesNotExist() throws Exception { Path file = createTempDir().resolve(randomAlphaOfLength(10)); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map users = FileUserPasswdStore.parseFile(file, logger, Settings.EMPTY); assertThat(users, nullValue()); users = FileUserPasswdStore.parseFileLenient(file, logger, Settings.EMPTY); @@ -207,7 +207,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { Path file = createTempFile(); // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); try { FileUserPasswdStore.parseFile(file, logger, Settings.EMPTY); fail("expected a parse failure"); @@ -228,7 +228,7 @@ public class FileUserPasswdStoreTests extends ESTestCase { Path file = createTempFile(); // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map users = FileUserPasswdStore.parseFileLenient(file, logger, Settings.EMPTY); assertThat(users, notNullValue()); assertThat(users.isEmpty(), is(true)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java index f987f4df572..8e4011b1159 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/file/FileUserRolesStoreTests.java @@ -175,7 +175,7 @@ public class FileUserRolesStoreTests extends ESTestCase { public void testParseFileEmpty() throws Exception { Path empty = createTempFile(); - Logger log = CapturingLogger.newCapturingLogger(Level.DEBUG); + Logger log = CapturingLogger.newCapturingLogger(Level.DEBUG, null); FileUserRolesStore.parseFile(empty, log); List events = CapturingLogger.output(log.getName(), Level.DEBUG); assertThat(events.size(), is(1)); @@ -184,7 +184,7 @@ public class FileUserRolesStoreTests extends ESTestCase { public void testParseFileWhenFileDoesNotExist() throws Exception { Path file = createTempDir().resolve(randomAlphaOfLength(10)); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map usersRoles = FileUserRolesStore.parseFile(file, logger); assertThat(usersRoles, nullValue()); usersRoles = FileUserRolesStore.parseFileLenient(file, logger); @@ -199,7 +199,7 @@ public class FileUserRolesStoreTests extends ESTestCase { // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, lines, StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); try { FileUserRolesStore.parseFile(file, logger); fail("expected a parse failure"); @@ -256,7 +256,7 @@ public class FileUserRolesStoreTests extends ESTestCase { // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, lines, StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); Map usersRoles = FileUserRolesStore.parseFileLenient(file, logger); assertThat(usersRoles, notNullValue()); assertThat(usersRoles.isEmpty(), is(true)); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java index f6d18b7cfc1..263c5ee4929 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/DnRoleMapperTests.java @@ -199,7 +199,7 @@ public class DnRoleMapperTests extends ESTestCase { public void testParseFile() throws Exception { Path file = getDataPath("role_mapping.yml"); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.size(), is(3)); @@ -229,18 +229,19 @@ public class DnRoleMapperTests extends ESTestCase { public void testParseFile_Empty() throws Exception { Path file = createTempDir().resolve("foo.yaml"); Files.createFile(file); - Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG); + Logger logger = CapturingLogger.newCapturingLogger(Level.DEBUG, null); Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.DEBUG); assertThat(events.size(), is(1)); assertThat(events.get(0), containsString("[0] role mappings found")); + events.clear(); } public void testParseFile_WhenFileDoesNotExist() throws Exception { Path file = createTempDir().resolve(randomAlphaOfLength(10)); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map> mappings = DnRoleMapper.parseFile(file, logger, "_type", "_name", false); assertThat(mappings, notNullValue()); assertThat(mappings.isEmpty(), is(true)); @@ -257,7 +258,7 @@ public class DnRoleMapperTests extends ESTestCase { Path file = createTempFile("", ".yml"); // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); try { DnRoleMapper.parseFile(file, logger, "_type", "_name", false); fail("expected a parse failure"); @@ -270,13 +271,14 @@ public class DnRoleMapperTests extends ESTestCase { Path file = createTempFile("", ".yml"); // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 Files.write(file, Collections.singletonList("aldlfkjldjdflkjd"), StandardCharsets.UTF_16); - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); Map> mappings = DnRoleMapper.parseFileLenient(file, logger, "_type", "_name"); assertThat(mappings, notNullValue()); assertThat(mappings.isEmpty(), is(true)); List events = CapturingLogger.output(logger.getName(), Level.ERROR); assertThat(events.size(), is(1)); assertThat(events.get(0), containsString("failed to parse role mappings file")); + events.clear(); } public void testYaml() throws Exception { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java index 1e2428e7779..5cb93b898ba 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/FileRolesStoreTests.java @@ -237,7 +237,9 @@ public class FileRolesStoreTests extends ESTestCase { public void testParseFileWithFLSAndDLSDisabled() throws Exception { Path path = getDataPath("roles.yml"); - Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR); + Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR, null); + List events = CapturingLogger.output(logger.getName(), Level.ERROR); + events.clear(); Map roles = FileRolesStore.parseFile(path, logger, Settings.builder() .put(XPackSettings.DLS_FLS_ENABLED.getKey(), false) .build(), new XPackLicenseState(Settings.EMPTY)); @@ -247,7 +249,6 @@ public class FileRolesStoreTests extends ESTestCase { assertThat(roles.get("role_query"), nullValue()); assertThat(roles.get("role_query_fields"), nullValue()); - List events = CapturingLogger.output(logger.getName(), Level.ERROR); assertThat(events, hasSize(3)); assertThat( events.get(0), @@ -263,7 +264,9 @@ public class FileRolesStoreTests extends ESTestCase { public void testParseFileWithFLSAndDLSUnlicensed() throws Exception { Path path = getDataPath("roles.yml"); - Logger logger = CapturingLogger.newCapturingLogger(Level.WARN); + Logger logger = CapturingLogger.newCapturingLogger(Level.WARN, null); + List events = CapturingLogger.output(logger.getName(), Level.WARN); + events.clear(); XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isDocumentAndFieldLevelSecurityAllowed()).thenReturn(false); Map roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, licenseState); @@ -273,7 +276,6 @@ public class FileRolesStoreTests extends ESTestCase { assertNotNull(roles.get("role_query")); assertNotNull(roles.get("role_query_fields")); - List events = CapturingLogger.output(logger.getName(), Level.WARN); assertThat(events, hasSize(3)); assertThat( events.get(0), @@ -369,7 +371,9 @@ public class FileRolesStoreTests extends ESTestCase { public void testThatInvalidRoleDefinitions() throws Exception { Path path = getDataPath("invalid_roles.yml"); - Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR); + Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR, null); + List entries = CapturingLogger.output(logger.getName(), Level.ERROR); + entries.clear(); Map roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, new XPackLicenseState(Settings.EMPTY)); assertThat(roles.size(), is(1)); assertThat(roles, hasKey("valid_role")); @@ -379,7 +383,6 @@ public class FileRolesStoreTests extends ESTestCase { assertThat(role, notNullValue()); assertThat(role.names(), equalTo(new String[] { "valid_role" })); - List entries = CapturingLogger.output(logger.getName(), Level.ERROR); assertThat(entries, hasSize(6)); assertThat( entries.get(0), @@ -395,12 +398,13 @@ public class FileRolesStoreTests extends ESTestCase { public void testThatRoleNamesDoesNotResolvePermissions() throws Exception { Path path = getDataPath("invalid_roles.yml"); - Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR); + Logger logger = CapturingLogger.newCapturingLogger(Level.ERROR, null); + List events = CapturingLogger.output(logger.getName(), Level.ERROR); + events.clear(); Set roleNames = FileRolesStore.parseFileForRoleNames(path, logger); assertThat(roleNames.size(), is(6)); assertThat(roleNames, containsInAnyOrder("valid_role", "role1", "role2", "role3", "role4", "role5")); - List events = CapturingLogger.output(logger.getName(), Level.ERROR); assertThat(events, hasSize(1)); assertThat( events.get(0), @@ -408,9 +412,9 @@ public class FileRolesStoreTests extends ESTestCase { } public void testReservedRoles() throws Exception { - - Logger logger = CapturingLogger.newCapturingLogger(Level.INFO); - + Logger logger = CapturingLogger.newCapturingLogger(Level.INFO, null); + List events = CapturingLogger.output(logger.getName(), Level.ERROR); + events.clear(); Path path = getDataPath("reserved_roles.yml"); Map roles = FileRolesStore.parseFile(path, logger, Settings.EMPTY, new XPackLicenseState(Settings.EMPTY)); assertThat(roles, notNullValue()); @@ -418,7 +422,6 @@ public class FileRolesStoreTests extends ESTestCase { assertThat(roles, hasKey("admin")); - List events = CapturingLogger.output(logger.getName(), Level.ERROR); assertThat(events, notNullValue()); assertThat(events, hasSize(4)); // the system role will always be checked first diff --git a/x-pack/qa/sql/security/build.gradle b/x-pack/qa/sql/security/build.gradle index 15f7734f942..9e6cc4eab23 100644 --- a/x-pack/qa/sql/security/build.gradle +++ b/x-pack/qa/sql/security/build.gradle @@ -40,7 +40,7 @@ subprojects { integTestRunner { systemProperty 'tests.audit.logfile', - "${ -> integTest.nodes[0].homeDir}/logs/${ -> integTest.nodes[0].clusterName }_access.log" + "${ -> integTest.nodes[0].homeDir}/logs/${ -> integTest.nodes[0].clusterName }_audit.log" } runqa { diff --git a/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java b/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java index cb8afc876a4..e1c94ce463f 100644 --- a/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java +++ b/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/RestSqlSecurityIT.java @@ -248,14 +248,14 @@ public class RestSqlSecurityIT extends SqlSecurityTestCase { final Matcher runByRealmMatcher = realm.equals("default_file") ? Matchers.nullValue(String.class) : Matchers.is("default_file"); logCheckers.add( - m -> eventType.equals(m.get("event_type")) + m -> eventType.equals(m.get("event.action")) && action.equals(m.get("action")) - && principal.equals(m.get("principal")) - && realm.equals(m.get("realm")) - && runByPrincipalMatcher.matches(m.get("run_by_principal")) - && runByRealmMatcher.matches(m.get("run_by_realm")) + && principal.equals(m.get("user.name")) + && realm.equals(m.get("user.realm")) + && runByPrincipalMatcher.matches(m.get("user.run_by.name")) + && runByRealmMatcher.matches(m.get("user.run_by.realm")) && indicesMatcher.matches(m.get("indices")) - && request.equals(m.get("request"))); + && request.equals(m.get("request.name"))); return this; } diff --git a/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java b/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java index 83047c93da3..1bea73b56a2 100644 --- a/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java +++ b/x-pack/qa/sql/security/src/test/java/org/elasticsearch/xpack/qa/sql/security/SqlSecurityTestCase.java @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.qa.sql.security; import org.apache.lucene.util.SuppressForbidden; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.SpecialPermission; import org.elasticsearch.action.admin.indices.get.GetIndexAction; import org.elasticsearch.action.admin.indices.get.GetIndexRequest; @@ -14,6 +15,7 @@ import org.elasticsearch.client.ResponseException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.test.rest.ESRestTestCase; import org.hamcrest.Matcher; @@ -32,17 +34,16 @@ import java.security.PrivilegedAction; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.TreeMap; import java.util.function.Function; -import java.util.regex.Pattern; import static java.util.Collections.singletonMap; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.hasItems; public abstract class SqlSecurityTestCase extends ESRestTestCase { @@ -515,21 +516,22 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase { default: throw new IllegalArgumentException("Unknown action [" + action + "]"); } - final String eventType = granted ? "access_granted" : "access_denied"; + final String eventAction = granted ? "access_granted" : "access_denied"; final String realm = principal.equals("test_admin") ? "default_file" : "default_native"; - return expect(eventType, action, principal, realm, indicesMatcher, request); + return expect(eventAction, action, principal, realm, indicesMatcher, request); } - public AuditLogAsserter expect(String eventType, String action, String principal, String realm, + public AuditLogAsserter expect(String eventAction, String action, String principal, String realm, Matcher> indicesMatcher, String request) { - logCheckers.add(m -> eventType.equals(m.get("event_type")) + logCheckers.add(m -> + eventAction.equals(m.get("event.action")) && action.equals(m.get("action")) - && principal.equals(m.get("principal")) - && realm.equals(m.get("realm")) - && Matchers.nullValue(String.class).matches(m.get("run_by_principal")) - && Matchers.nullValue(String.class).matches(m.get("run_by_realm")) + && principal.equals(m.get("user.name")) + && realm.equals(m.get("user.realm")) + && Matchers.nullValue(String.class).matches(m.get("user.run_by.name")) + && Matchers.nullValue(String.class).matches(m.get("user.run_by.realm")) && indicesMatcher.matches(m.get("indices")) - && request.equals(m.get("request")) + && request.equals(m.get("request.name")) ); return this; } @@ -554,56 +556,39 @@ public abstract class SqlSecurityTestCase extends ESRestTestCase { List> logs = new ArrayList<>(); String line; - Pattern logPattern = Pattern.compile( - ("PART PART PART PART origin_type=PART, origin_address=PART, principal=PART, realm=PART, " - + "(?:run_as_principal=IGN, )?(?:run_as_realm=IGN, )?(?:run_by_principal=PART, )?(?:run_by_realm=PART, )?" - + "roles=PART, action=\\[(.*?)\\], (?:indices=PART, )?request=PART") - .replace(" ", "\\s+").replace("PART", "\\[([^\\]]*)\\]").replace("IGN", "\\[[^\\]]*\\]")); - // fail(logPattern.toString()); while ((line = logReader.readLine()) != null) { - java.util.regex.Matcher m = logPattern.matcher(line); - if (false == m.matches()) { - throw new IllegalArgumentException("Unrecognized log: " + line); + try { + final Map log = XContentHelper.convertToMap(JsonXContent.jsonXContent, line, false); + if (false == ("access_denied".equals(log.get("event.action")) + || "access_granted".equals(log.get("event.action")))) { + continue; + } + assertThat(log.containsKey("action"), is(true)); + if (false == (SQL_ACTION_NAME.equals(log.get("action")) || GetIndexAction.NAME.equals(log.get("action")))) { + // TODO we may want to extend this and the assertions to SearchAction.NAME as well + continue; + } + assertThat(log.containsKey("user.name"), is(true)); + List indices = new ArrayList<>(); + if (log.containsKey("indices")) { + indices = (ArrayList) log.get("indices"); + if ("test_admin".equals(log.get("user.name"))) { + /* + * Sometimes we accidentally sneak access to the security tables. This is fine, + * SQL drops them from the interface. So we might have access to them, but we + * don't show them. + */ + indices.remove(".security"); + indices.remove(".security-6"); + } + } + // Use a sorted list for indices for consistent error reporting + Collections.sort(indices); + log.put("indices", indices); + logs.add(log); + } catch (final ElasticsearchParseException e) { + throw new IllegalArgumentException("Unrecognized log: " + line, e); } - int i = 1; - Map log = new HashMap<>(); - /* We *could* parse the date but leaving it in the original format makes it - * easier to find the lines in the file that this log comes from. */ - log.put("time", m.group(i++)); - log.put("node", m.group(i++)); - log.put("origin", m.group(i++)); - String eventType = m.group(i++); - if (false == ("access_denied".equals(eventType) || "access_granted".equals(eventType))) { - continue; - } - log.put("event_type", eventType); - log.put("origin_type", m.group(i++)); - log.put("origin_address", m.group(i++)); - String principal = m.group(i++); - log.put("principal", principal); - log.put("realm", m.group(i++)); - log.put("run_by_principal", m.group(i++)); - log.put("run_by_realm", m.group(i++)); - log.put("roles", m.group(i++)); - String action = m.group(i++); - if (false == (SQL_ACTION_NAME.equals(action) || GetIndexAction.NAME.equals(action))) { - //TODO we may want to extend this and the assertions to SearchAction.NAME as well - continue; - } - log.put("action", action); - // Use a sorted list for indices for consistent error reporting - List indices = new ArrayList<>(Strings.tokenizeByCommaToSet(m.group(i++))); - Collections.sort(indices); - if ("test_admin".equals(principal)) { - /* Sometimes we accidentally sneak access to the security tables. This is fine, SQL - * drops them from the interface. So we might have access to them, but we don't show - * them. */ - indices.remove(".security"); - indices.remove(".security-6"); - } - log.put("indices", indices); - log.put("request", m.group(i)); - logs.add(log); } List> allLogs = new ArrayList<>(logs); List notMatching = new ArrayList<>(); From 140c3bb61ce33b3d46aeb36114d5a0d05dd02b20 Mon Sep 17 00:00:00 2001 From: markharwood Date: Fri, 14 Sep 2018 13:46:36 +0100 Subject: [PATCH 35/45] Test fix - Graph vertices could appear in different orders based on map insertion sequence (#33709) Solved by sorting the vertices arrays before comparison Closes #33686 --- .../xpack/graph/GraphExploreResponseTests.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java index 9c4a76fdcec..fb3b483d23a 100644 --- a/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java +++ b/x-pack/protocol/src/test/java/org/elasticsearch/protocol/xpack/graph/GraphExploreResponseTests.java @@ -26,6 +26,8 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.function.Predicate; @@ -105,8 +107,17 @@ public class GraphExploreResponseTests extends AbstractXContentTestCase< GraphEx Connection[] expectedConns = expectedInstance.getConnections().toArray(new Connection[0]); assertArrayEquals(expectedConns, newConns); - Vertex[] newVertices = newInstance.getVertices().toArray(new Vertex[0]); + //Sort the vertices lists before equality test (map insertion sequences can cause order differences) + Comparator comparator = new Comparator() { + @Override + public int compare(Vertex o1, Vertex o2) { + return o1.getId().toString().compareTo(o2.getId().toString()); + } + }; + Vertex[] newVertices = newInstance.getVertices().toArray(new Vertex[0]); Vertex[] expectedVertices = expectedInstance.getVertices().toArray(new Vertex[0]); + Arrays.sort(newVertices, comparator); + Arrays.sort(expectedVertices, comparator); assertArrayEquals(expectedVertices, newVertices); ShardOperationFailedException[] newFailures = newInstance.getShardFailures(); From 2282150f3407d28a797a79df11ab7b6fc6c36203 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 14 Sep 2018 08:52:46 -0400 Subject: [PATCH 36/45] Expose retries for CCR fetch failures (#33694) This commit exposes the number of times that a fetch has been tried to the CCR stats endpoint, and to CCR monitoring. --- .../xpack/ccr/action/ShardFollowNodeTask.java | 17 +++++++---- .../ShardFollowNodeTaskStatusTests.java | 25 ++++++++++------ .../ccr/action/ShardFollowNodeTaskTests.java | 22 +++++++------- .../core/ccr/ShardFollowNodeTaskStatus.java | 30 ++++++++++++------- .../src/main/resources/monitoring-es.json | 3 ++ .../ccr/CcrStatsMonitoringDocTests.java | 14 ++++++--- 6 files changed, 72 insertions(+), 39 deletions(-) diff --git a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java index f88f21e4072..7faebfdc26c 100644 --- a/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java +++ b/x-pack/plugin/ccr/src/main/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTask.java @@ -11,6 +11,7 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.TransportActions; import org.elasticsearch.common.Randomness; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.transport.NetworkExceptionHelper; import org.elasticsearch.common.unit.TimeValue; @@ -36,6 +37,7 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.function.LongSupplier; +import java.util.stream.Collectors; /** * The node task that fetch the write operations from a leader shard and @@ -72,7 +74,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { private long numberOfOperationsIndexed = 0; private long lastFetchTime = -1; private final Queue buffer = new PriorityQueue<>(Comparator.comparing(Translog.Operation::seqNo)); - private final LinkedHashMap fetchExceptions; + private final LinkedHashMap> fetchExceptions; ShardFollowNodeTask(long id, String type, String action, String description, TaskId parentTask, Map headers, ShardFollowTask params, BiConsumer scheduler, final LongSupplier relativeTimeProvider) { @@ -87,9 +89,9 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { * concurrent fetches. For each failed fetch, we track the from sequence number associated with the request, and we clear the entry * when the fetch task associated with that from sequence number succeeds. */ - this.fetchExceptions = new LinkedHashMap() { + this.fetchExceptions = new LinkedHashMap>() { @Override - protected boolean removeEldestEntry(final Map.Entry eldest) { + protected boolean removeEldestEntry(final Map.Entry> eldest) { return size() > params.getMaxConcurrentReadBatches(); } }; @@ -240,7 +242,7 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { synchronized (ShardFollowNodeTask.this) { totalFetchTimeMillis += TimeUnit.NANOSECONDS.toMillis(relativeTimeProvider.getAsLong() - startTime); numberOfFailedFetches++; - fetchExceptions.put(from, new ElasticsearchException(e)); + fetchExceptions.put(from, Tuple.tuple(retryCounter, new ElasticsearchException(e))); } handleFailure(e, retryCounter, () -> sendShardChangesRequest(from, maxOperationCount, maxRequiredSeqNo, retryCounter)); }); @@ -438,7 +440,12 @@ public abstract class ShardFollowNodeTask extends AllocatedPersistentTask { numberOfSuccessfulBulkOperations, numberOfFailedBulkOperations, numberOfOperationsIndexed, - new TreeMap<>(fetchExceptions), + new TreeMap<>( + fetchExceptions + .entrySet() + .stream() + .collect( + Collectors.toMap(Map.Entry::getKey, e -> Tuple.tuple(e.getValue().v1().get(), e.getValue().v2())))), timeSinceLastFetchMillis); } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java index d5f2ab7ea08..48e4359ccb5 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskStatusTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; @@ -83,15 +84,17 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< assertThat(newInstance.numberOfOperationsIndexed(), equalTo(expectedInstance.numberOfOperationsIndexed())); assertThat(newInstance.fetchExceptions().size(), equalTo(expectedInstance.fetchExceptions().size())); assertThat(newInstance.fetchExceptions().keySet(), equalTo(expectedInstance.fetchExceptions().keySet())); - for (final Map.Entry entry : newInstance.fetchExceptions().entrySet()) { + for (final Map.Entry> entry : newInstance.fetchExceptions().entrySet()) { + final Tuple expectedTuple = expectedInstance.fetchExceptions().get(entry.getKey()); + assertThat(entry.getValue().v1(), equalTo(expectedTuple.v1())); // x-content loses the exception - final ElasticsearchException expected = expectedInstance.fetchExceptions().get(entry.getKey()); - assertThat(entry.getValue().getMessage(), containsString(expected.getMessage())); - assertNotNull(entry.getValue().getCause()); + final ElasticsearchException expected = expectedTuple.v2(); + assertThat(entry.getValue().v2().getMessage(), containsString(expected.getMessage())); + assertNotNull(entry.getValue().v2().getCause()); assertThat( - entry.getValue().getCause(), + entry.getValue().v2().getCause(), anyOf(instanceOf(ElasticsearchException.class), instanceOf(IllegalStateException.class))); - assertThat(entry.getValue().getCause().getMessage(), containsString(expected.getCause().getMessage())); + assertThat(entry.getValue().v2().getCause().getMessage(), containsString(expected.getCause().getMessage())); } assertThat(newInstance.timeSinceLastFetchMillis(), equalTo(expectedInstance.timeSinceLastFetchMillis())); } @@ -101,11 +104,15 @@ public class ShardFollowNodeTaskStatusTests extends AbstractSerializingTestCase< return false; } - private NavigableMap randomReadExceptions() { + private NavigableMap> randomReadExceptions() { final int count = randomIntBetween(0, 16); - final NavigableMap readExceptions = new TreeMap<>(); + final NavigableMap> readExceptions = new TreeMap<>(); for (int i = 0; i < count; i++) { - readExceptions.put(randomNonNegativeLong(), new ElasticsearchException(new IllegalStateException("index [" + i + "]"))); + readExceptions.put( + randomNonNegativeLong(), + Tuple.tuple( + randomIntBetween(0, Integer.MAX_VALUE), + new ElasticsearchException(new IllegalStateException("index [" + i + "]")))); } return readExceptions; } diff --git a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java index 101b2580759..71a97bf8207 100644 --- a/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/ShardFollowNodeTaskTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardNotFoundException; @@ -192,12 +193,13 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(status.numberOfFailedFetches(), equalTo(retryCounter.get())); if (retryCounter.get() > 0) { assertThat(status.fetchExceptions().entrySet(), hasSize(1)); - final Map.Entry entry = status.fetchExceptions().entrySet().iterator().next(); + final Map.Entry> entry = status.fetchExceptions().entrySet().iterator().next(); + assertThat(entry.getValue().v1(), equalTo(Math.toIntExact(retryCounter.get()))); assertThat(entry.getKey(), equalTo(0L)); - assertThat(entry.getValue(), instanceOf(ElasticsearchException.class)); - assertNotNull(entry.getValue().getCause()); - assertThat(entry.getValue().getCause(), instanceOf(ShardNotFoundException.class)); - final ShardNotFoundException cause = (ShardNotFoundException) entry.getValue().getCause(); + assertThat(entry.getValue().v2(), instanceOf(ElasticsearchException.class)); + assertNotNull(entry.getValue().v2().getCause()); + assertThat(entry.getValue().v2().getCause(), instanceOf(ShardNotFoundException.class)); + final ShardNotFoundException cause = (ShardNotFoundException) entry.getValue().v2().getCause(); assertThat(cause.getShardId().getIndexName(), equalTo("leader_index")); assertThat(cause.getShardId().getId(), equalTo(0)); } @@ -253,12 +255,12 @@ public class ShardFollowNodeTaskTests extends ESTestCase { assertThat(status.numberOfConcurrentWrites(), equalTo(0)); assertThat(status.numberOfFailedFetches(), equalTo(1L)); assertThat(status.fetchExceptions().entrySet(), hasSize(1)); - final Map.Entry entry = status.fetchExceptions().entrySet().iterator().next(); + final Map.Entry> entry = status.fetchExceptions().entrySet().iterator().next(); assertThat(entry.getKey(), equalTo(0L)); - assertThat(entry.getValue(), instanceOf(ElasticsearchException.class)); - assertNotNull(entry.getValue().getCause()); - assertThat(entry.getValue().getCause(), instanceOf(RuntimeException.class)); - final RuntimeException cause = (RuntimeException) entry.getValue().getCause(); + assertThat(entry.getValue().v2(), instanceOf(ElasticsearchException.class)); + assertNotNull(entry.getValue().v2().getCause()); + assertThat(entry.getValue().v2().getCause(), instanceOf(RuntimeException.class)); + final RuntimeException cause = (RuntimeException) entry.getValue().v2().getCause(); assertThat(cause.getMessage(), equalTo("replication failed")); assertThat(status.lastRequestedSeqNo(), equalTo(63L)); assertThat(status.leaderGlobalCheckpoint(), equalTo(63L)); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java index dafb4a5e29f..a8193c35a8d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ccr/ShardFollowNodeTaskStatus.java @@ -9,6 +9,7 @@ package org.elasticsearch.xpack.core.ccr; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.unit.ByteSizeUnit; @@ -84,17 +85,17 @@ public class ShardFollowNodeTaskStatus implements Task.Status { (long) args[19], (long) args[20], new TreeMap<>( - ((List>) args[21]) + ((List>>) args[21]) .stream() .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))), (long) args[22])); public static final String FETCH_EXCEPTIONS_ENTRY_PARSER_NAME = "shard-follow-node-task-status-fetch-exceptions-entry"; - static final ConstructingObjectParser, Void> FETCH_EXCEPTIONS_ENTRY_PARSER = + static final ConstructingObjectParser>, Void> FETCH_EXCEPTIONS_ENTRY_PARSER = new ConstructingObjectParser<>( FETCH_EXCEPTIONS_ENTRY_PARSER_NAME, - args -> new AbstractMap.SimpleEntry<>((long) args[0], (ElasticsearchException) args[1])); + args -> new AbstractMap.SimpleEntry<>((long) args[0], Tuple.tuple((Integer)args[1], (ElasticsearchException)args[2]))); static { STATUS_PARSER.declareString(ConstructingObjectParser.constructorArg(), LEADER_INDEX); @@ -123,10 +124,12 @@ public class ShardFollowNodeTaskStatus implements Task.Status { } static final ParseField FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO = new ParseField("from_seq_no"); + static final ParseField FETCH_EXCEPTIONS_RETRIES = new ParseField("retries"); static final ParseField FETCH_EXCEPTIONS_ENTRY_EXCEPTION = new ParseField("exception"); static { FETCH_EXCEPTIONS_ENTRY_PARSER.declareLong(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO); + FETCH_EXCEPTIONS_ENTRY_PARSER.declareInt(ConstructingObjectParser.constructorArg(), FETCH_EXCEPTIONS_RETRIES); FETCH_EXCEPTIONS_ENTRY_PARSER.declareObject( ConstructingObjectParser.constructorArg(), (p, c) -> ElasticsearchException.fromXContent(p), @@ -259,9 +262,9 @@ public class ShardFollowNodeTaskStatus implements Task.Status { return numberOfOperationsIndexed; } - private final NavigableMap fetchExceptions; + private final NavigableMap> fetchExceptions; - public NavigableMap fetchExceptions() { + public NavigableMap> fetchExceptions() { return fetchExceptions; } @@ -293,7 +296,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { final long numberOfSuccessfulBulkOperations, final long numberOfFailedBulkOperations, final long numberOfOperationsIndexed, - final NavigableMap fetchExceptions, + final NavigableMap> fetchExceptions, final long timeSinceLastFetchMillis) { this.leaderIndex = leaderIndex; this.followerIndex = followerIndex; @@ -342,7 +345,8 @@ public class ShardFollowNodeTaskStatus implements Task.Status { this.numberOfSuccessfulBulkOperations = in.readVLong(); this.numberOfFailedBulkOperations = in.readVLong(); this.numberOfOperationsIndexed = in.readVLong(); - this.fetchExceptions = new TreeMap<>(in.readMap(StreamInput::readVLong, StreamInput::readException)); + this.fetchExceptions = + new TreeMap<>(in.readMap(StreamInput::readVLong, stream -> Tuple.tuple(stream.readVInt(), stream.readException()))); this.timeSinceLastFetchMillis = in.readZLong(); } @@ -374,7 +378,10 @@ public class ShardFollowNodeTaskStatus implements Task.Status { out.writeVLong(numberOfSuccessfulBulkOperations); out.writeVLong(numberOfFailedBulkOperations); out.writeVLong(numberOfOperationsIndexed); - out.writeMap(fetchExceptions, StreamOutput::writeVLong, StreamOutput::writeException); + out.writeMap( + fetchExceptions, + StreamOutput::writeVLong, + (stream, value) -> { stream.writeVInt(value.v1()); stream.writeException(value.v2()); }); out.writeZLong(timeSinceLastFetchMillis); } @@ -421,14 +428,15 @@ public class ShardFollowNodeTaskStatus implements Task.Status { builder.field(NUMBER_OF_OPERATIONS_INDEXED_FIELD.getPreferredName(), numberOfOperationsIndexed); builder.startArray(FETCH_EXCEPTIONS.getPreferredName()); { - for (final Map.Entry entry : fetchExceptions.entrySet()) { + for (final Map.Entry> entry : fetchExceptions.entrySet()) { builder.startObject(); { builder.field(FETCH_EXCEPTIONS_ENTRY_FROM_SEQ_NO.getPreferredName(), entry.getKey()); + builder.field(FETCH_EXCEPTIONS_RETRIES.getPreferredName(), entry.getValue().v1()); builder.field(FETCH_EXCEPTIONS_ENTRY_EXCEPTION.getPreferredName()); builder.startObject(); { - ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue()); + ElasticsearchException.generateThrowableXContent(builder, params, entry.getValue().v2()); } builder.endObject(); } @@ -515,7 +523,7 @@ public class ShardFollowNodeTaskStatus implements Task.Status { } private static List getFetchExceptionMessages(final ShardFollowNodeTaskStatus status) { - return status.fetchExceptions().values().stream().map(ElasticsearchException::getMessage).collect(Collectors.toList()); + return status.fetchExceptions().values().stream().map(t -> t.v2().getMessage()).collect(Collectors.toList()); } public String toString() { diff --git a/x-pack/plugin/core/src/main/resources/monitoring-es.json b/x-pack/plugin/core/src/main/resources/monitoring-es.json index 83c9fe70e11..444f15912e6 100644 --- a/x-pack/plugin/core/src/main/resources/monitoring-es.json +++ b/x-pack/plugin/core/src/main/resources/monitoring-es.json @@ -987,6 +987,9 @@ "from_seq_no": { "type": "long" }, + "retries": { + "type": "integer" + }, "exception": { "type": "text" } diff --git a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java index ed893410c88..9124e1d5245 100644 --- a/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java +++ b/x-pack/plugin/monitoring/src/test/java/org/elasticsearch/xpack/monitoring/collector/ccr/CcrStatsMonitoringDocTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.xpack.monitoring.collector.ccr; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; @@ -101,8 +102,10 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase fetchExceptions = - new TreeMap<>(Collections.singletonMap(randomNonNegativeLong(), new ElasticsearchException("shard is sad"))); + final NavigableMap> fetchExceptions = + new TreeMap<>(Collections.singletonMap( + randomNonNegativeLong(), + Tuple.tuple(randomIntBetween(0, Integer.MAX_VALUE), new ElasticsearchException("shard is sad")))); final long timeSinceLastFetchMillis = randomNonNegativeLong(); final ShardFollowNodeTaskStatus status = new ShardFollowNodeTaskStatus( "cluster_alias:leader_index", @@ -171,6 +174,7 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase fetchExceptions = - new TreeMap<>(Collections.singletonMap(1L, new ElasticsearchException("shard is sad"))); + final NavigableMap> fetchExceptions = + new TreeMap<>(Collections.singletonMap(1L, Tuple.tuple(2, new ElasticsearchException("shard is sad")))); final ShardFollowNodeTaskStatus status = new ShardFollowNodeTaskStatus( "cluster_alias:leader_index", "follower_index", @@ -234,7 +238,9 @@ public class CcrStatsMonitoringDocTests extends BaseMonitoringDocTestCase) fieldMapping.get("properties")).size(), equalTo(3)); assertThat(XContentMapValues.extractValue("properties.from_seq_no.type", fieldMapping), equalTo("long")); + assertThat(XContentMapValues.extractValue("properties.retries.type", fieldMapping), equalTo("integer")); assertThat(XContentMapValues.extractValue("properties.exception.type", fieldMapping), equalTo("text")); } else { fail("unexpected field value type [" + fieldValue.getClass() + "] for field [" + fieldName + "]"); From 4f68104865bb5f36b473a223a88ca8e1ed4feff4 Mon Sep 17 00:00:00 2001 From: Adrien Grand Date: Fri, 14 Sep 2018 14:59:16 +0200 Subject: [PATCH 37/45] Don't count hits via the collector if the hit count can be computed from index stats. (#33701) This is something that we were already doing when sorting by field, which is now also done when sorting by score. As-is this change will speed up top-k `term` queries. This could work for `match_all` queries as well when we implement the `setMinCompetitiveScore` API on their Scorer. --- .../search/query/TopDocsCollectorContext.java | 74 +++++++++---------- 1 file changed, 35 insertions(+), 39 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java b/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java index 3aaa640f62f..fcf70a4f98c 100644 --- a/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java +++ b/server/src/main/java/org/elasticsearch/search/query/TopDocsCollectorContext.java @@ -174,6 +174,16 @@ abstract class TopDocsCollectorContext extends QueryCollectorContext { } abstract static class SimpleTopDocsCollectorContext extends TopDocsCollectorContext { + + private static TopDocsCollector createCollector(@Nullable SortAndFormats sortAndFormats, int numHits, + @Nullable ScoreDoc searchAfter, int hitCountThreshold) { + if (sortAndFormats == null) { + return TopScoreDocCollector.create(numHits, searchAfter, hitCountThreshold); + } else { + return TopFieldCollector.create(sortAndFormats.sort, numHits, (FieldDoc) searchAfter, hitCountThreshold); + } + } + private final @Nullable SortAndFormats sortAndFormats; private final Collector collector; private final Supplier totalHitsSupplier; @@ -201,12 +211,27 @@ abstract class TopDocsCollectorContext extends QueryCollectorContext { boolean hasFilterCollector) throws IOException { super(REASON_SEARCH_TOP_HITS, numHits); this.sortAndFormats = sortAndFormats; + + // implicit total hit counts are valid only when there is no filter collector in the chain + final int hitCount = hasFilterCollector ? -1 : shortcutTotalHitCount(reader, query); + final TopDocsCollector topDocsCollector; + if (hitCount == -1 && trackTotalHits) { + topDocsCollector = createCollector(sortAndFormats, numHits, searchAfter, Integer.MAX_VALUE); + topDocsSupplier = new CachedSupplier<>(topDocsCollector::topDocs); + totalHitsSupplier = () -> topDocsSupplier.get().totalHits; + } else { + topDocsCollector = createCollector(sortAndFormats, numHits, searchAfter, 1); // don't compute hit counts via the collector + topDocsSupplier = new CachedSupplier<>(topDocsCollector::topDocs); + if (hitCount == -1) { + assert trackTotalHits == false; + totalHitsSupplier = () -> new TotalHits(0, TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO); + } else { + totalHitsSupplier = () -> new TotalHits(hitCount, TotalHits.Relation.EQUAL_TO); + } + } + MaxScoreCollector maxScoreCollector = null; if (sortAndFormats == null) { - final TopDocsCollector topDocsCollector = TopScoreDocCollector.create(numHits, searchAfter, Integer.MAX_VALUE); - this.collector = topDocsCollector; - this.topDocsSupplier = new CachedSupplier<>(topDocsCollector::topDocs); - this.totalHitsSupplier = () -> topDocsSupplier.get().totalHits; - this.maxScoreSupplier = () -> { + maxScoreSupplier = () -> { TopDocs topDocs = topDocsSupplier.get(); if (topDocs.scoreDocs.length == 0) { return Float.NaN; @@ -214,42 +239,13 @@ abstract class TopDocsCollectorContext extends QueryCollectorContext { return topDocs.scoreDocs[0].score; } }; + } else if (trackMaxScore) { + maxScoreCollector = new MaxScoreCollector(); + maxScoreSupplier = maxScoreCollector::getMaxScore; } else { - /** - * We explicitly don't track total hits in the topdocs collector, it can early terminate - * if the sort matches the index sort. - */ - final TopDocsCollector topDocsCollector = TopFieldCollector.create(sortAndFormats.sort, numHits, - (FieldDoc) searchAfter, 1); - this.topDocsSupplier = new CachedSupplier<>(topDocsCollector::topDocs); - TotalHitCountCollector hitCountCollector = null; - if (trackTotalHits) { - // implicit total hit counts are valid only when there is no filter collector in the chain - int count = hasFilterCollector ? -1 : shortcutTotalHitCount(reader, query); - if (count != -1) { - // we can extract the total count from the shard statistics directly - this.totalHitsSupplier = () -> new TotalHits(count, TotalHits.Relation.EQUAL_TO); - } else { - // wrap a collector that counts the total number of hits even - // if the top docs collector terminates early - final TotalHitCountCollector countingCollector = new TotalHitCountCollector(); - hitCountCollector = countingCollector; - this.totalHitsSupplier = () -> new TotalHits(countingCollector.getTotalHits(), TotalHits.Relation.EQUAL_TO); - } - } else { - // total hit count is not needed - // for bwc hit count is set to 0, it will be converted to -1 by the coordinating node - this.totalHitsSupplier = () -> new TotalHits(0, TotalHits.Relation.GREATER_THAN_OR_EQUAL_TO); - } - MaxScoreCollector maxScoreCollector = null; - if (trackMaxScore) { - maxScoreCollector = new MaxScoreCollector(); - maxScoreSupplier = maxScoreCollector::getMaxScore; - } else { - maxScoreSupplier = () -> Float.NaN; - } - collector = MultiCollector.wrap(topDocsCollector, hitCountCollector, maxScoreCollector); + maxScoreSupplier = () -> Float.NaN; } + this.collector = MultiCollector.wrap(topDocsCollector, maxScoreCollector); } @Override From 39191331d19e00312f0de2db850fd72c92dafdee Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 14 Sep 2018 09:32:03 -0400 Subject: [PATCH 38/45] Only notify ready global checkpoint listeners (#33690) When we add a global checkpoint listener, it is also carries along with it a value that it thinks is the current global checkpoint. This value can be above the actual global checkpoint on a shard if the listener knows the global checkpoint from another shard copy (e.g., the primary), and the current shard copy is lagging behind. Today we notify the listener whenever the global checkpoint advances, regardless if it goes above the current global checkpoint known to the listener. This commit reworks this implementation. Rather than thinking of the value associated with the listener as the current global checkpoint known to the listener, we think of it as the value that the listener is waiting for the global checkpoint to advance to (inclusive). Now instead of notifying all waiting listeners when the global checkpoint advances, we only notify those that are waiting for a value not larger than the actual global checkpoint that we advanced to. --- .../shard/GlobalCheckpointListeners.java | 130 ++++++------ .../elasticsearch/index/shard/IndexShard.java | 17 +- .../shard/GlobalCheckpointListenersTests.java | 197 ++++++++++-------- .../index/shard/IndexShardIT.java | 8 +- 4 files changed, 186 insertions(+), 166 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index 224d5be17e1..bedd1654449 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,11 +21,13 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; import java.io.Closeable; import java.io.IOException; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; @@ -34,6 +36,7 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.stream.Collectors; import static org.elasticsearch.index.seqno.SequenceNumbers.NO_OPS_PERFORMED; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; @@ -63,7 +66,7 @@ public class GlobalCheckpointListeners implements Closeable { // guarded by this private boolean closed; - private Map> listeners; + private final Map>> listeners = new LinkedHashMap<>(); private long lastKnownGlobalCheckpoint = UNASSIGNED_SEQ_NO; private final ShardId shardId; @@ -91,62 +94,56 @@ public class GlobalCheckpointListeners implements Closeable { } /** - * Add a global checkpoint listener. If the global checkpoint is above the current global checkpoint known to the listener then the - * listener will be asynchronously notified on the executor used to construct this collection of global checkpoint listeners. If the - * shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global - * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated or the shard - * is closed. A listener must re-register after one of these events to receive subsequent events. Callers may add a timeout to be - * notified after if the timeout elapses. In this case, the listener will be notified with a {@link TimeoutException}. Passing null for - * the timeout means no timeout will be associated to the listener. + * Add a global checkpoint listener. If the global checkpoint is equal to or above the global checkpoint the listener is waiting for, + * then the listener will be asynchronously notified on the executor used to construct this collection of global checkpoint listeners. + * If the shard is closed then the listener will be asynchronously notified on the executor used to construct this collection of global + * checkpoint listeners. The listener will only be notified of at most one event, either the global checkpoint is updated above the + * global checkpoint the listener is waiting for, or the shard is closed. A listener must re-register after one of these events to + * receive subsequent events. Callers may add a timeout to be notified after if the timeout elapses. In this case, the listener will be + * notified with a {@link TimeoutException}. Passing null fo the timeout means no timeout will be associated to the listener. * - * @param currentGlobalCheckpoint the current global checkpoint known to the listener - * @param listener the listener - * @param timeout the listener timeout, or null if no timeout + * @param waitingForGlobalCheckpoint the current global checkpoint known to the listener + * @param listener the listener + * @param timeout the listener timeout, or null if no timeout */ - synchronized void add(final long currentGlobalCheckpoint, final GlobalCheckpointListener listener, final TimeValue timeout) { + synchronized void add(final long waitingForGlobalCheckpoint, final GlobalCheckpointListener listener, final TimeValue timeout) { if (closed) { executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, new IndexShardClosedException(shardId))); return; } - if (lastKnownGlobalCheckpoint > currentGlobalCheckpoint) { + if (lastKnownGlobalCheckpoint >= waitingForGlobalCheckpoint) { // notify directly executor.execute(() -> notifyListener(listener, lastKnownGlobalCheckpoint, null)); } else { - if (listeners == null) { - listeners = new LinkedHashMap<>(); - } if (timeout == null) { - listeners.put(listener, null); + listeners.put(listener, Tuple.tuple(waitingForGlobalCheckpoint, null)); } else { listeners.put( listener, - scheduler.schedule( - () -> { - final boolean removed; - synchronized (this) { - /* - * Note that the listeners map can be null if a notification nulled out the map reference when - * notifying listeners, and then our scheduled execution occurred before we could be cancelled by - * the notification. In this case, we would have blocked waiting for access to this critical - * section. - * - * What is more, we know that this listener has a timeout associated with it (otherwise we would - * not be here) so the return value from remove being null is an indication that we are not in the - * map. This can happen if a notification nulled out the listeners, and then our scheduled execution - * occurred before we could be cancelled by the notification, and then another thread added a - * listener causing the listeners map reference to be non-null again. In this case, our listener - * here would not be in the map and we should not fire the timeout logic. - */ - removed = listeners != null && listeners.remove(listener) != null; - } - if (removed) { - final TimeoutException e = new TimeoutException(timeout.getStringRep()); - logger.trace("global checkpoint listener timed out", e); - executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); - } - }, - timeout.nanos(), - TimeUnit.NANOSECONDS)); + Tuple.tuple( + waitingForGlobalCheckpoint, + scheduler.schedule( + () -> { + final boolean removed; + synchronized (this) { + /* + * We know that this listener has a timeout associated with it (otherwise we would not be + * here) so the future component of the return value from remove being null is an indication + * that we are not in the map. This can happen if a notification collected us into listeners + * to be notified and removed us from the map, and then our scheduled execution occurred + * before we could be cancelled by the notification. In this case, our listener here would + * not be in the map and we should not fire the timeout logic. + */ + removed = listeners.remove(listener).v2() != null; + } + if (removed) { + final TimeoutException e = new TimeoutException(timeout.getStringRep()); + logger.trace("global checkpoint listener timed out", e); + executor.execute(() -> notifyListener(listener, UNASSIGNED_SEQ_NO, e)); + } + }, + timeout.nanos(), + TimeUnit.NANOSECONDS))); } } } @@ -163,7 +160,7 @@ public class GlobalCheckpointListeners implements Closeable { * @return the number of listeners pending notification */ synchronized int pendingListeners() { - return listeners == null ? 0 : listeners.size(); + return listeners.size(); } /** @@ -173,7 +170,7 @@ public class GlobalCheckpointListeners implements Closeable { * @return a scheduled future representing the timeout future for the listener, otherwise null */ synchronized ScheduledFuture getTimeoutFuture(final GlobalCheckpointListener listener) { - return listeners.get(listener); + return listeners.get(listener).v2(); } /** @@ -193,22 +190,31 @@ public class GlobalCheckpointListeners implements Closeable { private void notifyListeners(final long globalCheckpoint, final IndexShardClosedException e) { assert Thread.holdsLock(this); assert (globalCheckpoint == UNASSIGNED_SEQ_NO && e != null) || (globalCheckpoint >= NO_OPS_PERFORMED && e == null); - if (listeners != null) { - // capture the current listeners - final Map> currentListeners = listeners; - listeners = null; - if (currentListeners != null) { - executor.execute(() -> { - for (final Map.Entry> listener : currentListeners.entrySet()) { - /* - * We do not want to interrupt any timeouts that fired, these will detect that the listener has been notified and - * not trigger the timeout. - */ - FutureUtils.cancel(listener.getValue()); - notifyListener(listener.getKey(), globalCheckpoint, e); - } - }); - } + + final Map>> listenersToNotify; + if (globalCheckpoint != UNASSIGNED_SEQ_NO) { + listenersToNotify = + listeners + .entrySet() + .stream() + .filter(entry -> entry.getValue().v1() <= globalCheckpoint) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + listenersToNotify.keySet().forEach(listeners::remove); + } else { + listenersToNotify = new HashMap<>(listeners); + listeners.clear(); + } + if (listenersToNotify.isEmpty() == false) { + executor.execute(() -> + listenersToNotify + .forEach((listener, t) -> { + /* + * We do not want to interrupt any timeouts that fired, these will detect that the listener has been + * notified and not trigger the timeout. + */ + FutureUtils.cancel(t.v2()); + notifyListener(listener, globalCheckpoint, e); + })); } } diff --git a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java index 91d87b00082..168444a2267 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java +++ b/server/src/main/java/org/elasticsearch/index/shard/IndexShard.java @@ -1781,19 +1781,20 @@ public class IndexShard extends AbstractIndexShardComponent implements IndicesCl } /** - * Add a global checkpoint listener. If the global checkpoint is above the current global checkpoint known to the listener then the - * listener will fire immediately on the calling thread. If the specified timeout elapses before the listener is notified, the listener - * will be notified with an {@link TimeoutException}. A caller may pass null to specify no timeout. + * Add a global checkpoint listener. If the global checkpoint is equal to or above the global checkpoint the listener is waiting for, + * then the listener will be notified immediately via an executor (so possibly not on the current thread). If the specified timeout + * elapses before the listener is notified, the listener will be notified with an {@link TimeoutException}. A caller may pass null to + * specify no timeout. * - * @param currentGlobalCheckpoint the current global checkpoint known to the listener - * @param listener the listener - * @param timeout the timeout + * @param waitingForGlobalCheckpoint the global checkpoint the listener is waiting for + * @param listener the listener + * @param timeout the timeout */ public void addGlobalCheckpointListener( - final long currentGlobalCheckpoint, + final long waitingForGlobalCheckpoint, final GlobalCheckpointListeners.GlobalCheckpointListener listener, final TimeValue timeout) { - this.globalCheckpointListeners.add(currentGlobalCheckpoint, listener, timeout); + this.globalCheckpointListeners.add(waitingForGlobalCheckpoint, listener, timeout); } /** diff --git a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java index 4ab278cc02a..fa0e0cee143 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java +++ b/server/src/test/java/org/elasticsearch/index/shard/GlobalCheckpointListenersTests.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Assertions; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.EsExecutors; @@ -31,7 +32,9 @@ import org.mockito.ArgumentCaptor; import java.io.IOException; import java.io.UncheckedIOException; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.BrokenBarrierException; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; @@ -76,62 +79,70 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); globalCheckpointListeners.globalCheckpointUpdated(NO_OPS_PERFORMED); - final int numberOfListeners = randomIntBetween(0, 16); - final long[] globalCheckpoints = new long[numberOfListeners]; + final int numberOfListeners = randomIntBetween(0, 64); + final Map listeners = new HashMap<>(); + final Map notifiedListeners = new HashMap<>(); for (int i = 0; i < numberOfListeners; i++) { - final int index = i; - final AtomicBoolean invoked = new AtomicBoolean(); - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (g, e) -> { - if (invoked.compareAndSet(false, true) == false) { - throw new IllegalStateException("listener invoked twice"); - } - assert g != UNASSIGNED_SEQ_NO; - assert e == null; - globalCheckpoints[index] = g; - }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); + final GlobalCheckpointListeners.GlobalCheckpointListener listener = new GlobalCheckpointListeners.GlobalCheckpointListener() { + @Override + public void accept(final long g, final Exception e) { + notifiedListeners.put(this, g); + } + }; + final long waitingGlobalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); + listeners.put(listener, waitingGlobalCheckpoint); + globalCheckpointListeners.add(waitingGlobalCheckpoint, maybeMultipleInvocationProtectingListener(listener), null); } - final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); + final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE - 1); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); - for (int i = 0; i < numberOfListeners; i++) { - assertThat(globalCheckpoints[i], equalTo(globalCheckpoint)); + for (final Map.Entry listener : listeners.entrySet()) { + if (listener.getValue() <= globalCheckpoint) { + // only listeners waiting on a lower global checkpoint will have been notified + assertThat(notifiedListeners.get(listener.getKey()), equalTo(globalCheckpoint)); + } else { + assertNull(notifiedListeners.get(listener.getKey())); + } } // test the listeners are not invoked twice - final long nextGlobalCheckpoint = randomLongBetween(globalCheckpoint + 1, Long.MAX_VALUE); + notifiedListeners.clear(); + final long nextGlobalCheckpoint = randomLongBetween(1 + globalCheckpoint, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(nextGlobalCheckpoint); - for (int i = 0; i < numberOfListeners; i++) { - assertThat(globalCheckpoints[i], equalTo(globalCheckpoint)); + for (final Map.Entry listener : listeners.entrySet()) { + if (listener.getValue() > globalCheckpoint && listener.getValue() <= nextGlobalCheckpoint) { + // these listeners will have been notified by the second global checkpoint update, and all the other listeners should not be + assertThat(notifiedListeners.get(listener.getKey()), equalTo(nextGlobalCheckpoint)); + } else { + assertNull(notifiedListeners.get(listener.getKey())); + } } // closing should also not notify the listeners + notifiedListeners.clear(); globalCheckpointListeners.close(); - for (int i = 0; i < numberOfListeners; i++) { - assertThat(globalCheckpoints[i], equalTo(globalCheckpoint)); + for (final Map.Entry listener : listeners.entrySet()) { + if (listener.getValue() > nextGlobalCheckpoint) { + // these listeners should have been notified that we closed, and all the other listeners should not be + assertThat(notifiedListeners.get(listener.getKey()), equalTo(UNASSIGNED_SEQ_NO)); + } else { + assertNull(notifiedListeners.get(listener.getKey())); + } } } public void testListenersReadyToBeNotified() throws IOException { final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); - final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED + 1, Long.MAX_VALUE); + final long globalCheckpoint = randomLongBetween(0, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); final int numberOfListeners = randomIntBetween(0, 16); final long[] globalCheckpoints = new long[numberOfListeners]; for (int i = 0; i < numberOfListeners; i++) { final int index = i; - final AtomicBoolean invoked = new AtomicBoolean(); - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (g, e) -> { - if (invoked.compareAndSet(false, true) == false) { - throw new IllegalStateException("listener invoked twice"); - } - assert g != UNASSIGNED_SEQ_NO; - assert e == null; - globalCheckpoints[index] = g; - }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener, null); + globalCheckpointListeners.add( + randomLongBetween(0, globalCheckpoint), + maybeMultipleInvocationProtectingListener((g, e) -> globalCheckpoints[index] = g), + null); // the listener should be notified immediately assertThat(globalCheckpoints[index], equalTo(globalCheckpoint)); } @@ -161,18 +172,17 @@ public class GlobalCheckpointListenersTests extends ESTestCase { for (int i = 0; i < numberOfListeners; i++) { final int index = i; final boolean failure = randomBoolean(); - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (g, e) -> { - assert globalCheckpoint != UNASSIGNED_SEQ_NO; - assert e == null; + globalCheckpointListeners.add( + randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), + maybeMultipleInvocationProtectingListener((g, e) -> { if (failure) { globalCheckpoints[index] = Long.MIN_VALUE; throw new RuntimeException("failure"); } else { globalCheckpoints[index] = globalCheckpoint; } - }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, globalCheckpoint - 1), listener, null); + }), + null); // the listener should be notified immediately if (failure) { assertThat(globalCheckpoints[i], equalTo(Long.MIN_VALUE)); @@ -202,17 +212,8 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final Exception[] exceptions = new Exception[numberOfListeners]; for (int i = 0; i < numberOfListeners; i++) { final int index = i; - final AtomicBoolean invoked = new AtomicBoolean(); - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (globalCheckpoint, e) -> { - if (invoked.compareAndSet(false, true) == false) { - throw new IllegalStateException("listener invoked twice"); - } - assert globalCheckpoint == UNASSIGNED_SEQ_NO; - assert e != null; - exceptions[index] = e; - }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); + globalCheckpointListeners.add( + 0, maybeMultipleInvocationProtectingListener((g, e) -> exceptions[index] = e), null); } globalCheckpointListeners.close(); for (int i = 0; i < numberOfListeners; i++) { @@ -238,16 +239,13 @@ public class GlobalCheckpointListenersTests extends ESTestCase { globalCheckpointListeners.close(); final AtomicBoolean invoked = new AtomicBoolean(); final CountDownLatch latch = new CountDownLatch(1); - final GlobalCheckpointListeners.GlobalCheckpointListener listener = (g, e) -> { - assert g == UNASSIGNED_SEQ_NO; - assert e != null; - if (invoked.compareAndSet(false, true) == false) { - latch.countDown(); - throw new IllegalStateException("listener invoked twice"); - } - latch.countDown(); - }; - globalCheckpointListeners.add(randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE), listener, null); + globalCheckpointListeners.add( + randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE), + maybeMultipleInvocationProtectingListener((g, e) -> { + invoked.set(true); + latch.countDown(); + }), + null); latch.await(); assertTrue(invoked.get()); } @@ -264,18 +262,17 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final int index = i; final boolean failure = randomBoolean(); failures[index] = failure; - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (g, e) -> { - assert g != UNASSIGNED_SEQ_NO; - assert e == null; + globalCheckpointListeners.add( + 0, + maybeMultipleInvocationProtectingListener((g, e) -> { if (failure) { globalCheckpoints[index] = Long.MIN_VALUE; throw new RuntimeException("failure"); } else { globalCheckpoints[index] = g; } - }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); + }), + null); } final long globalCheckpoint = randomLongBetween(NO_OPS_PERFORMED, Long.MAX_VALUE); globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); @@ -319,17 +316,16 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final int index = i; final boolean failure = randomBoolean(); failures[index] = failure; - final GlobalCheckpointListeners.GlobalCheckpointListener listener = - (g, e) -> { - assert g == UNASSIGNED_SEQ_NO; - assert e != null; + globalCheckpointListeners.add( + 0, + maybeMultipleInvocationProtectingListener((g, e) -> { if (failure) { throw new RuntimeException("failure"); } else { exceptions[index] = e; } - }; - globalCheckpointListeners.add(NO_OPS_PERFORMED, listener, null); + }), + null); } globalCheckpointListeners.close(); for (int i = 0; i < numberOfListeners; i++) { @@ -370,12 +366,12 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final int numberOfListeners = randomIntBetween(0, 16); for (int i = 0; i < numberOfListeners; i++) { globalCheckpointListeners.add( - NO_OPS_PERFORMED, - (g, e) -> { + 0, + maybeMultipleInvocationProtectingListener((g, e) -> { notified.incrementAndGet(); assertThat(g, equalTo(globalCheckpoint)); assertNull(e); - }, + }), null); } globalCheckpointListeners.globalCheckpointUpdated(globalCheckpoint); @@ -396,13 +392,13 @@ public class GlobalCheckpointListenersTests extends ESTestCase { for (int i = 0; i < numberOfListeners; i++) { globalCheckpointListeners.add( NO_OPS_PERFORMED, - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { notified.incrementAndGet(); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); assertNotNull(e); assertThat(e, instanceOf(IndexShardClosedException.class)); assertThat(((IndexShardClosedException) e).getShardId(), equalTo(shardId)); - }, + }), null); } assertThat(notified.get(), equalTo(numberOfListeners)); @@ -423,11 +419,12 @@ public class GlobalCheckpointListenersTests extends ESTestCase { for (int i = 0; i < numberOfListeners; i++) { globalCheckpointListeners.add( randomLongBetween(0, globalCheckpoint), - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { notified.incrementAndGet(); assertThat(g, equalTo(globalCheckpoint)); assertNull(e); - }, null); + }), + null); } assertThat(notified.get(), equalTo(numberOfListeners)); assertThat(count.get(), equalTo(numberOfListeners)); @@ -472,11 +469,11 @@ public class GlobalCheckpointListenersTests extends ESTestCase { // sometimes this will notify the listener immediately globalCheckpointListeners.add( globalCheckpoint.get(), - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { if (invocation.compareAndSet(false, true) == false) { throw new IllegalStateException("listener invoked twice"); } - }, + }), randomBoolean() ? null : TimeValue.timeValueNanos(randomLongBetween(1, TimeUnit.MICROSECONDS.toNanos(1)))); } // synchronize ending with the updating thread and the main test thread @@ -511,7 +508,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final CountDownLatch latch = new CountDownLatch(1); globalCheckpointListeners.add( NO_OPS_PERFORMED, - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); @@ -527,7 +524,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } finally { latch.countDown(); } - }, + }), timeout); latch.await(); @@ -546,7 +543,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final CountDownLatch latch = new CountDownLatch(1); globalCheckpointListeners.add( NO_OPS_PERFORMED, - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { try { notified.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); @@ -554,7 +551,7 @@ public class GlobalCheckpointListenersTests extends ESTestCase { } finally { latch.countDown(); } - }, + }), timeout); latch.await(); // ensure the listener notification occurred on the executor @@ -574,9 +571,9 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); globalCheckpointListeners.add( NO_OPS_PERFORMED, - (g, e) -> { + maybeMultipleInvocationProtectingListener((g, e) -> { throw new RuntimeException("failure"); - }, + }), timeout); latch.await(); final ArgumentCaptor message = ArgumentCaptor.forClass(String.class); @@ -592,10 +589,11 @@ public class GlobalCheckpointListenersTests extends ESTestCase { final GlobalCheckpointListeners globalCheckpointListeners = new GlobalCheckpointListeners(shardId, Runnable::run, scheduler, logger); final TimeValue timeout = TimeValue.timeValueNanos(Long.MAX_VALUE); - final GlobalCheckpointListeners.GlobalCheckpointListener globalCheckpointListener = (g, e) -> { - assertThat(g, equalTo(NO_OPS_PERFORMED)); - assertNull(e); - }; + final GlobalCheckpointListeners.GlobalCheckpointListener globalCheckpointListener = + maybeMultipleInvocationProtectingListener((g, e) -> { + assertThat(g, equalTo(NO_OPS_PERFORMED)); + assertNull(e); + }); globalCheckpointListeners.add(NO_OPS_PERFORMED, globalCheckpointListener, timeout); final ScheduledFuture future = globalCheckpointListeners.getTimeoutFuture(globalCheckpointListener); assertNotNull(future); @@ -603,6 +601,21 @@ public class GlobalCheckpointListenersTests extends ESTestCase { assertTrue(future.isCancelled()); } + private GlobalCheckpointListeners.GlobalCheckpointListener maybeMultipleInvocationProtectingListener( + final GlobalCheckpointListeners.GlobalCheckpointListener globalCheckpointListener) { + if (Assertions.ENABLED) { + final AtomicBoolean invoked = new AtomicBoolean(); + return (g, e) -> { + if (invoked.compareAndSet(false, true) == false) { + throw new AssertionError("listener invoked twice"); + } + globalCheckpointListener.accept(g, e); + }; + } else { + return globalCheckpointListener; + } + } + private void awaitQuietly(final CyclicBarrier barrier) { try { barrier.await(); diff --git a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java index 2c659ac60ec..56a14da845f 100644 --- a/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java +++ b/server/src/test/java/org/elasticsearch/index/shard/IndexShardIT.java @@ -747,7 +747,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { final int index = i; final AtomicLong globalCheckpoint = new AtomicLong(); shard.addGlobalCheckpointListener( - i - 1, + i, (g, e) -> { assertThat(g, greaterThanOrEqualTo(NO_OPS_PERFORMED)); assertNull(e); @@ -759,7 +759,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { // adding a listener expecting a lower global checkpoint should fire immediately final AtomicLong immediateGlobalCheckpint = new AtomicLong(); shard.addGlobalCheckpointListener( - randomLongBetween(NO_OPS_PERFORMED, i - 1), + randomLongBetween(0, i), (g, e) -> { assertThat(g, greaterThanOrEqualTo(NO_OPS_PERFORMED)); assertNull(e); @@ -770,7 +770,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { } final AtomicBoolean invoked = new AtomicBoolean(); shard.addGlobalCheckpointListener( - numberOfUpdates - 1, + numberOfUpdates, (g, e) -> { invoked.set(true); assertThat(g, equalTo(UNASSIGNED_SEQ_NO)); @@ -792,7 +792,7 @@ public class IndexShardIT extends ESSingleNodeTestCase { final CountDownLatch latch = new CountDownLatch(1); final TimeValue timeout = TimeValue.timeValueMillis(randomIntBetween(1, 50)); shard.addGlobalCheckpointListener( - NO_OPS_PERFORMED, + 0, (g, e) -> { try { notified.set(true); From d9f5e4fd2e06c9b69f3b4744e49e747e1ff708b4 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 14 Sep 2018 13:49:35 +0300 Subject: [PATCH 39/45] Pin TLS1.2 in SSLConfigurationReloaderTests Ensure that the SSLConfigurationReloaderTests can run with JDK 11 by pinning the HttpClient to TLS version to TLS1.2. This is necessary becase even if the MockWebServer is set to user TLS1.2, we don't set its enabled protocols, so if it receives a TLS1.3 request (which is the default behavior for HttpClient in JDK11), it will use TLS1.3 and the original issue will manifest again. Relates #33127 Resolves #32124 --- .../xpack/core/ssl/SSLConfigurationReloaderTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java index df25b2fa126..9202207a14e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ssl/SSLConfigurationReloaderTests.java @@ -510,7 +510,7 @@ public class SSLConfigurationReloaderTests extends ESTestCase { try (InputStream is = Files.newInputStream(trustStorePath)) { trustStore.load(is, trustStorePass.toCharArray()); } - final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(trustStore, null).build(); + final SSLContext sslContext = new SSLContextBuilder().useProtocol("TLSv1.2").loadTrustMaterial(trustStore, null).build(); return HttpClients.custom().setSSLContext(sslContext).build(); } @@ -527,7 +527,7 @@ public class SSLConfigurationReloaderTests extends ESTestCase { for (Certificate cert : CertParsingUtils.readCertificates(trustedCertificatePaths)) { trustStore.setCertificateEntry(cert.toString(), cert); } - final SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(trustStore, null).build(); + final SSLContext sslContext = new SSLContextBuilder().useProtocol("TLSv1.2").loadTrustMaterial(trustStore, null).build(); return HttpClients.custom().setSSLContext(sslContext).build(); } From bcbbbdf660cb1df4a846be39fecc879b73267e06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20B=C3=BCscher?= Date: Fri, 14 Sep 2018 15:52:47 +0200 Subject: [PATCH 40/45] [Tests] Fix randomization in StringTermsIT (#33678) It looks like the COLLECT_SEGMENT_ORDS flag should be randomized. --- .../search/aggregations/bucket/terms/StringTermsIT.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java index c92681d99a9..3ef91ff4753 100644 --- a/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java +++ b/server/src/test/java/org/elasticsearch/search/aggregations/bucket/terms/StringTermsIT.java @@ -38,8 +38,8 @@ import org.elasticsearch.search.aggregations.bucket.AbstractTermsTestCase; import org.elasticsearch.search.aggregations.bucket.filter.Filter; import org.elasticsearch.search.aggregations.bucket.terms.Terms.Bucket; import org.elasticsearch.search.aggregations.metrics.Avg; -import org.elasticsearch.search.aggregations.metrics.Stats; import org.elasticsearch.search.aggregations.metrics.ExtendedStats; +import org.elasticsearch.search.aggregations.metrics.Stats; import org.elasticsearch.search.aggregations.metrics.Sum; import org.elasticsearch.test.ESIntegTestCase; import org.hamcrest.Matchers; @@ -87,7 +87,7 @@ public class StringTermsIT extends AbstractTermsTestCase { @Before public void randomizeOptimizations() { - TermsAggregatorFactory.COLLECT_SEGMENT_ORDS = false;randomBoolean(); + TermsAggregatorFactory.COLLECT_SEGMENT_ORDS = randomBoolean(); TermsAggregatorFactory.REMAP_GLOBAL_ORDS = randomBoolean(); } From b04faa059b50bfd8c55a5704c5bca432439ec85d Mon Sep 17 00:00:00 2001 From: David Kyle Date: Fri, 14 Sep 2018 15:00:18 +0100 Subject: [PATCH 41/45] HLRC: ML PUT Calendar (#33362) --- .../client/MLRequestConverters.java | 13 + .../client/MachineLearningClient.java | 467 +++++++++--------- .../client/ml/PutCalendarRequest.java | 73 +++ .../client/ml/PutCalendarResponse.java | 76 +++ .../client/ml/calendars/Calendar.java | 115 +++++ .../client/ml/calendars/ScheduledEvent.java | 125 +++++ .../client/MLRequestConvertersTests.java | 14 + .../client/MachineLearningIT.java | 14 + .../MlClientDocumentationIT.java | 69 ++- .../client/ml/PutCalendarRequestTests.java | 44 ++ .../client/ml/PutCalendarResponseTests.java | 43 ++ .../client/ml/calendars/CalendarTests.java | 61 +++ .../ml/calendars/ScheduledEventTests.java | 51 ++ .../high-level/ml/get-buckets.asciidoc | 2 +- .../high-level/ml/get-categories.asciidoc | 2 +- .../high-level/ml/get-influencers.asciidoc | 2 +- .../ml/get-overall-buckets.asciidoc | 2 +- .../high-level/ml/get-records.asciidoc | 2 +- .../high-level/ml/put-calendar.asciidoc | 65 +++ .../high-level/supported-apis.asciidoc | 2 + 20 files changed, 1001 insertions(+), 241 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarResponse.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/Calendar.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/ScheduledEvent.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarRequestTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarResponseTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/CalendarTests.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/ScheduledEventTests.java create mode 100644 docs/java-rest/high-level/ml/put-calendar.asciidoc diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java index 81b1f6b5709..731a4d41378 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MLRequestConverters.java @@ -42,6 +42,7 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest; import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PutCalendarRequest; import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.UpdateJobRequest; @@ -327,4 +328,16 @@ final class MLRequestConverters { request.setEntity(createEntity(getInfluencersRequest, REQUEST_BODY_CONTENT_TYPE)); return request; } + + static Request putCalendar(PutCalendarRequest putCalendarRequest) throws IOException { + String endpoint = new EndpointBuilder() + .addPathPartAsIs("_xpack") + .addPathPartAsIs("ml") + .addPathPartAsIs("calendars") + .addPathPart(putCalendarRequest.getCalendar().getId()) + .build(); + Request request = new Request(HttpPut.METHOD_NAME, endpoint); + request.setEntity(createEntity(putCalendarRequest, REQUEST_BODY_CONTENT_TYPE)); + return request; + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java index 4d2167ce063..0fd397bba89 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/MachineLearningClient.java @@ -47,6 +47,8 @@ import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; import org.elasticsearch.client.ml.PostDataRequest; import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutCalendarRequest; +import org.elasticsearch.client.ml.PutCalendarResponse; import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; @@ -60,7 +62,6 @@ import java.util.Collections; /** * Machine Learning API client wrapper for the {@link RestHighLevelClient} - * *

* See the * X-Pack Machine Learning APIs for additional information. @@ -86,10 +87,10 @@ public final class MachineLearningClient { */ public PutJobResponse putJob(PutJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::putJob, - options, - PutJobResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::putJob, + options, + PutJobResponse::fromXContent, + Collections.emptySet()); } /** @@ -104,63 +105,60 @@ public final class MachineLearningClient { */ public void putJobAsync(PutJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::putJob, - options, - PutJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::putJob, + options, + PutJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** * Gets one or more Machine Learning job configuration info. - * *

- * For additional info - * see - *

+ * For additional info + * see ML GET job documentation + * * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return {@link GetJobResponse} response object containing * the {@link org.elasticsearch.client.ml.job.config.Job} objects and the number of jobs found * @throws IOException when there is a serialization issue sending the request or receiving the response */ public GetJobResponse getJob(GetJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::getJob, - options, - GetJobResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::getJob, + options, + GetJobResponse::fromXContent, + Collections.emptySet()); } - /** + /** * Gets one or more Machine Learning job configuration info, asynchronously. - * *

- * For additional info - * see - *

- * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional options + * For additional info + * see ML GET job documentation + * + * @param request {@link GetJobRequest} Request containing a list of jobId(s) and additional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified with {@link GetJobResponse} upon request completion */ public void getJobAsync(GetJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::getJob, - options, - GetJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::getJob, + options, + GetJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** * Gets usage statistics for one or more Machine Learning jobs - * *

- * For additional info - * see Get Job stats docs - *

+ * For additional info + * see Get job stats docs + * * @param request {@link GetJobStatsRequest} Request containing a list of jobId(s) and additional options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return {@link GetJobStatsResponse} response object containing * the {@link JobStats} objects and the number of jobs found * @throws IOException when there is a serialization issue sending the request or receiving the response @@ -175,12 +173,11 @@ public final class MachineLearningClient { /** * Gets one or more Machine Learning job configuration info, asynchronously. - * *

- * For additional info - * see Get Job stats docs - *

- * @param request {@link GetJobStatsRequest} Request containing a list of jobId(s) and additional options + * For additional info + * see Get job stats docs + * + * @param request {@link GetJobStatsRequest} Request containing a list of jobId(s) and additional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified with {@link GetJobStatsResponse} upon request completion */ @@ -196,11 +193,11 @@ public final class MachineLearningClient { /** * Deletes the given Machine Learning Job *

- * For additional info - * see ML Delete Job documentation - *

+ * For additional info + * see ML Delete job documentation + * * @param request The request to delete the job - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return action acknowledgement * @throws IOException when there is a serialization issue sending the request or receiving the response */ @@ -215,10 +212,10 @@ public final class MachineLearningClient { /** * Deletes the given Machine Learning Job asynchronously and notifies the listener on completion *

- * For additional info - * see ML Delete Job documentation - *

- * @param request The request to delete the job + * For additional info + * see ML Delete Job documentation + * + * @param request The request to delete the job * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ @@ -234,103 +231,101 @@ public final class MachineLearningClient { /** * Opens a Machine Learning Job. * When you open a new job, it starts with an empty model. - * * When you open an existing job, the most recent model state is automatically loaded. * The job is ready to resume its analysis from where it left off, once new data is received. - * *

- * For additional info - * see - *

+ * For additional info + * see ML Open Job documentation + * * @param request Request containing job_id and additional optional options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing if the job was successfully opened or not. * @throws IOException when there is a serialization issue sending the request or receiving the response */ public OpenJobResponse openJob(OpenJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::openJob, - options, - OpenJobResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::openJob, + options, + OpenJobResponse::fromXContent, + Collections.emptySet()); } /** * Opens a Machine Learning Job asynchronously, notifies listener on completion. * When you open a new job, it starts with an empty model. - * * When you open an existing job, the most recent model state is automatically loaded. * The job is ready to resume its analysis from where it left off, once new data is received. *

- * For additional info - * see - *

- * @param request Request containing job_id and additional optional options + * For additional info + * see ML Open Job documentation + * + * @param request Request containing job_id and additional optional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ public void openJobAsync(OpenJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::openJob, - options, - OpenJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::openJob, + options, + OpenJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** * Closes one or more Machine Learning Jobs. A job can be opened and closed multiple times throughout its lifecycle. - * * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. + *

+ * For additional info + * see ML Close Job documentation * * @param request Request containing job_ids and additional options. See {@link CloseJobRequest} - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing if the job was successfully closed or not. * @throws IOException when there is a serialization issue sending the request or receiving the response */ public CloseJobResponse closeJob(CloseJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::closeJob, - options, - CloseJobResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::closeJob, + options, + CloseJobResponse::fromXContent, + Collections.emptySet()); } /** * Closes one or more Machine Learning Jobs asynchronously, notifies listener on completion - * * A closed job cannot receive data or perform analysis operations, but you can still explore and navigate results. + *

+ * For additional info + * see ML Close Job documentation * - * @param request Request containing job_ids and additional options. See {@link CloseJobRequest} + * @param request Request containing job_ids and additional options. See {@link CloseJobRequest} * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ public void closeJobAsync(CloseJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::closeJob, - options, - CloseJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::closeJob, + options, + CloseJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** * Flushes internally buffered data for the given Machine Learning Job ensuring all data sent to the has been processed. * This may cause new results to be calculated depending on the contents of the buffer - * * Both flush and close operations are similar, * however the flush is more efficient if you are expecting to send more data for analysis. - * * When flushing, the job remains open and is available to continue analyzing data. * A close operation additionally prunes and persists the model state to disk and the * job must be opened again before analyzing further data. - * *

* For additional info * see Flush ML job documentation * - * @param request The {@link FlushJobRequest} object enclosing the `jobId` and additional request options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request The {@link FlushJobRequest} object enclosing the `jobId` and additional request options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @throws IOException when there is a serialization issue sending the request or receiving the response */ public FlushJobResponse flushJob(FlushJobRequest request, RequestOptions options) throws IOException { @@ -344,14 +339,11 @@ public final class MachineLearningClient { /** * Flushes internally buffered data for the given Machine Learning Job asynchronously ensuring all data sent to the has been processed. * This may cause new results to be calculated depending on the contents of the buffer - * * Both flush and close operations are similar, * however the flush is more efficient if you are expecting to send more data for analysis. - * * When flushing, the job remains open and is available to continue analyzing data. * A close operation additionally prunes and persists the model state to disk and the * job must be opened again before analyzing further data. - * *

* For additional info * see Flush ML job documentation @@ -371,87 +363,82 @@ public final class MachineLearningClient { /** * Creates a forecast of an existing, opened Machine Learning Job - * * This predicts the future behavior of a time series by using its historical behavior. - * *

- * For additional info - * see Forecast ML Job Documentation - *

+ * For additional info + * see Forecast ML Job Documentation + * * @param request ForecastJobRequest with forecasting options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing forecast acknowledgement and new forecast's ID * @throws IOException when there is a serialization issue sending the request or receiving the response */ public ForecastJobResponse forecastJob(ForecastJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::forecastJob, - options, - ForecastJobResponse::fromXContent, - Collections.emptySet()); - } - - /** - * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} - * - *

- * For additional info - * see - *

- * - * @param request the {@link UpdateJobRequest} object enclosing the desired updates - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return a PutJobResponse object containing the updated job object - * @throws IOException when there is a serialization issue sending the request or receiving the response - */ - public PutJobResponse updateJob(UpdateJobRequest request, RequestOptions options) throws IOException { - return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::updateJob, - options, - PutJobResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::forecastJob, + options, + ForecastJobResponse::fromXContent, + Collections.emptySet()); } /** * Creates a forecast of an existing, opened Machine Learning Job asynchronously - * * This predicts the future behavior of a time series by using its historical behavior. - * *

- * For additional info - * see Forecast ML Job Documentation - *

- * @param request ForecastJobRequest with forecasting options + * For additional info + * see Forecast ML Job Documentation + * + * @param request ForecastJobRequest with forecasting options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ public void forecastJobAsync(ForecastJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::forecastJob, - options, - ForecastJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::forecastJob, + options, + ForecastJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** - * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} asynchronously - * + * Deletes Machine Learning Job Forecasts *

- * For additional info - * see - *

- * @param request the {@link UpdateJobRequest} object enclosing the desired updates + * For additional info + * see Delete Job Forecast + * Documentation + * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return a AcknowledgedResponse object indicating request success + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public AcknowledgedResponse deleteForecast(DeleteForecastRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Deletes Machine Learning Job Forecasts asynchronously + *

+ * For additional info + * see Delete Job Forecast + * Documentation + * + * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ - public void updateJobAsync(UpdateJobRequest request, RequestOptions options, ActionListener listener) { + public void deleteForecastAsync(DeleteForecastRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::updateJob, - options, - PutJobResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::deleteForecast, + options, + AcknowledgedResponse::fromXContent, + listener, + Collections.emptySet()); } /** @@ -495,10 +482,10 @@ public final class MachineLearningClient { /** * Deletes the given Machine Learning Datafeed *

- * For additional info - * see - * ML Delete Datafeed documentation - *

+ * For additional info + * see + * ML Delete Datafeed documentation + * * @param request The request to delete the datafeed * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return action acknowledgement @@ -515,10 +502,10 @@ public final class MachineLearningClient { /** * Deletes the given Machine Learning Datafeed asynchronously and notifies the listener on completion *

- * For additional info - * see + * For additional info + * see * ML Delete Datafeed documentation - *

+ * * @param request The request to delete the datafeed * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion @@ -533,45 +520,41 @@ public final class MachineLearningClient { } /** - * Deletes Machine Learning Job Forecasts - * + * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} *

- * For additional info - * see - *

+ * For additional info + * see ML Update Job Documentation * - * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized - * @return a AcknowledgedResponse object indicating request success + * @param request the {@link UpdateJobRequest} object enclosing the desired updates + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return a PutJobResponse object containing the updated job object * @throws IOException when there is a serialization issue sending the request or receiving the response */ - public AcknowledgedResponse deleteForecast(DeleteForecastRequest request, RequestOptions options) throws IOException { + public PutJobResponse updateJob(UpdateJobRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::deleteForecast, - options, - AcknowledgedResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::updateJob, + options, + PutJobResponse::fromXContent, + Collections.emptySet()); } /** - * Deletes Machine Learning Job Forecasts asynchronously - * + * Updates a Machine Learning {@link org.elasticsearch.client.ml.job.config.Job} asynchronously *

- * For additional info - * see - *

+ * For additional info + * see ML Update Job Documentation * - * @param request the {@link DeleteForecastRequest} object enclosing the desired jobId, forecastIDs, and other options + * @param request the {@link UpdateJobRequest} object enclosing the desired updates * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ - public void deleteForecastAsync(DeleteForecastRequest request, RequestOptions options, ActionListener listener) { + public void updateJobAsync(UpdateJobRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::deleteForecast, - options, - AcknowledgedResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::updateJob, + options, + PutJobResponse::fromXContent, + listener, + Collections.emptySet()); } /** @@ -580,8 +563,8 @@ public final class MachineLearningClient { * For additional info * see ML GET buckets documentation * - * @param request The request - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized */ public GetBucketsResponse getBuckets(GetBucketsRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, @@ -608,25 +591,25 @@ public final class MachineLearningClient { GetBucketsResponse::fromXContent, listener, Collections.emptySet()); - } + } /** * Gets the categories for a Machine Learning Job. *

* For additional info * see - * ML GET categories documentation + * ML GET categories documentation * - * @param request The request - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @throws IOException when there is a serialization issue sending the request or receiving the response */ public GetCategoriesResponse getCategories(GetCategoriesRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::getCategories, - options, - GetCategoriesResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::getCategories, + options, + GetCategoriesResponse::fromXContent, + Collections.emptySet()); } /** @@ -634,7 +617,7 @@ public final class MachineLearningClient { *

* For additional info * see - * ML GET categories documentation + * ML GET categories documentation * * @param request The request * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized @@ -642,11 +625,11 @@ public final class MachineLearningClient { */ public void getCategoriesAsync(GetCategoriesRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::getCategories, - options, - GetCategoriesResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::getCategories, + options, + GetCategoriesResponse::fromXContent, + listener, + Collections.emptySet()); } /** @@ -654,10 +637,10 @@ public final class MachineLearningClient { *

* For additional info * see - * ML GET overall buckets documentation + * ML GET overall buckets documentation * - * @param request The request - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized */ public GetOverallBucketsResponse getOverallBuckets(GetOverallBucketsRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, @@ -672,7 +655,7 @@ public final class MachineLearningClient { *

* For additional info * see - * ML GET overall buckets documentation + * ML GET overall buckets documentation * * @param request The request * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized @@ -694,8 +677,8 @@ public final class MachineLearningClient { * For additional info * see ML GET records documentation * - * @param request the request - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request the request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized */ public GetRecordsResponse getRecords(GetRecordsRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, @@ -726,48 +709,44 @@ public final class MachineLearningClient { /** * Sends data to an anomaly detection job for analysis. - * - * NOTE: The job must have a state of open to receive and process the data. - * *

- * For additional info - * see ML POST Data documentation - *

+ * NOTE: The job must have a state of open to receive and process the data. + *

+ * For additional info + * see ML POST Data documentation * * @param request PostDataRequest containing the data to post and some additional options - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @return response containing operational progress about the job * @throws IOException when there is a serialization issue sending the request or receiving the response */ public PostDataResponse postData(PostDataRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, - MLRequestConverters::postData, - options, - PostDataResponse::fromXContent, - Collections.emptySet()); + MLRequestConverters::postData, + options, + PostDataResponse::fromXContent, + Collections.emptySet()); } /** * Sends data to an anomaly detection job for analysis, asynchronously - * - * NOTE: The job must have a state of open to receive and process the data. - * *

- * For additional info - * see ML POST Data documentation - *

+ * NOTE: The job must have a state of open to receive and process the data. + *

+ * For additional info + * see ML POST Data documentation * - * @param request PostDataRequest containing the data to post and some additional options + * @param request PostDataRequest containing the data to post and some additional options * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized * @param listener Listener to be notified upon request completion */ public void postDataAsync(PostDataRequest request, RequestOptions options, ActionListener listener) { restHighLevelClient.performRequestAsyncAndParseEntity(request, - MLRequestConverters::postData, - options, - PostDataResponse::fromXContent, - listener, - Collections.emptySet()); + MLRequestConverters::postData, + options, + PostDataResponse::fromXContent, + listener, + Collections.emptySet()); } /** @@ -775,10 +754,10 @@ public final class MachineLearningClient { *

* For additional info * see - * ML GET influencers documentation + * ML GET influencers documentation * - * @param request the request - * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param request the request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized */ public GetInfluencersResponse getInfluencers(GetInfluencersRequest request, RequestOptions options) throws IOException { return restHighLevelClient.performRequestAndParseEntity(request, @@ -793,7 +772,7 @@ public final class MachineLearningClient { *

* For additional info * * see - * ML GET influencers documentation + * ML GET influencers documentation * * @param request the request * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized @@ -808,4 +787,44 @@ public final class MachineLearningClient { listener, Collections.emptySet()); } + + /** + * Create a new machine learning calendar + *

+ * For additional info + * see + * ML create calendar documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return The {@link PutCalendarResponse} containing the calendar + * @throws IOException when there is a serialization issue sending the request or receiving the response + */ + public PutCalendarResponse putCalendar(PutCalendarRequest request, RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(request, + MLRequestConverters::putCalendar, + options, + PutCalendarResponse::fromXContent, + Collections.emptySet()); + } + + /** + * Create a new machine learning calendar, notifies listener with the created calendar + *

+ * For additional info + * see + * ML create calendar documentation + * + * @param request The request + * @param options Additional request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener Listener to be notified upon request completion + */ + public void putCalendarAsync(PutCalendarRequest request, RequestOptions options, ActionListener listener) { + restHighLevelClient.performRequestAsyncAndParseEntity(request, + MLRequestConverters::putCalendar, + options, + PutCalendarResponse::fromXContent, + listener, + Collections.emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarRequest.java new file mode 100644 index 00000000000..56d6b2b545b --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarRequest.java @@ -0,0 +1,73 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.client.ml.calendars.Calendar; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +/** + * Request to create a new Machine Learning calendar + */ +public class PutCalendarRequest extends ActionRequest implements ToXContentObject { + + private final Calendar calendar; + + public PutCalendarRequest(Calendar calendar) { + this.calendar = calendar; + } + + public Calendar getCalendar() { + return calendar; + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + calendar.toXContent(builder, params); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(calendar); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + PutCalendarRequest other = (PutCalendarRequest) obj; + return Objects.equals(calendar, other.calendar); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarResponse.java new file mode 100644 index 00000000000..31c056add98 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/PutCalendarResponse.java @@ -0,0 +1,76 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.calendars.Calendar; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +public class PutCalendarResponse implements ToXContentObject { + + public static PutCalendarResponse fromXContent(XContentParser parser) throws IOException { + return new PutCalendarResponse(Calendar.PARSER.parse(parser, null)); + } + + private final Calendar calendar; + + PutCalendarResponse(Calendar calendar) { + this.calendar = calendar; + } + + public Calendar getCalendar() { + return calendar; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + calendar.toXContent(builder, params); + return builder; + } + + @Override + public int hashCode() { + return Objects.hash(calendar); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + PutCalendarResponse other = (PutCalendarResponse) obj; + return Objects.equals(calendar, other.calendar); + } + + @Override + public final String toString() { + return Strings.toString(this); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/Calendar.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/Calendar.java new file mode 100644 index 00000000000..2116ce8de7d --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/Calendar.java @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml.calendars; + +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +/** + * A simple calendar object for scheduled (special) events. + * The calendar consists of a name an a list of job Ids or job groups + * the events are stored separately and reference the calendar. + */ +public class Calendar implements ToXContentObject { + + public static final String CALENDAR_TYPE = "calendar"; + + public static final ParseField JOB_IDS = new ParseField("job_ids"); + public static final ParseField ID = new ParseField("calendar_id"); + public static final ParseField DESCRIPTION = new ParseField("description"); + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>(CALENDAR_TYPE, true, a -> + new Calendar((String) a[0], (List) a[1], (String) a[2])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ID); + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), JOB_IDS); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), DESCRIPTION); + } + + private final String id; + private final List jobIds; + private final String description; + + /** + * {@code jobIds} can be a mix of job groups and job Ids + * @param id The calendar Id + * @param jobIds List of job Ids or job groups + * @param description An optional description + */ + public Calendar(String id, List jobIds, @Nullable String description) { + this.id = Objects.requireNonNull(id, ID.getPreferredName() + " must not be null"); + this.jobIds = Collections.unmodifiableList(Objects.requireNonNull(jobIds, JOB_IDS.getPreferredName() + " must not be null")); + this.description = description; + } + + public String getId() { + return id; + } + + public List getJobIds() { + return jobIds; + } + + @Nullable + public String getDescription() { + return description; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(ID.getPreferredName(), id); + builder.field(JOB_IDS.getPreferredName(), jobIds); + if (description != null) { + builder.field(DESCRIPTION.getPreferredName(), description); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + Calendar other = (Calendar) obj; + return id.equals(other.id) && jobIds.equals(other.jobIds) && Objects.equals(description, other.description); + } + + @Override + public int hashCode() { + return Objects.hash(id, jobIds, description); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/ScheduledEvent.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/ScheduledEvent.java new file mode 100644 index 00000000000..decaff728c6 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/ml/calendars/ScheduledEvent.java @@ -0,0 +1,125 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml.calendars; + +import org.elasticsearch.client.ml.job.util.TimeUtil; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.ObjectParser; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + + +import java.io.IOException; +import java.util.Date; +import java.util.Objects; + +public class ScheduledEvent implements ToXContentObject { + + public static final ParseField DESCRIPTION = new ParseField("description"); + public static final ParseField START_TIME = new ParseField("start_time"); + public static final ParseField END_TIME = new ParseField("end_time"); + public static final ParseField EVENT_ID = new ParseField("event_id"); + public static final String SCHEDULED_EVENT_TYPE = "scheduled_event"; + + public static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>(SCHEDULED_EVENT_TYPE, true, a -> + new ScheduledEvent((String) a[0], (Date) a[1], (Date) a[2], (String) a[3], (String) a[4])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), DESCRIPTION); + PARSER.declareField(ConstructingObjectParser.constructorArg(),(p) -> TimeUtil.parseTimeField(p, START_TIME.getPreferredName()), + START_TIME, ObjectParser.ValueType.VALUE); + PARSER.declareField(ConstructingObjectParser.constructorArg(),(p) -> TimeUtil.parseTimeField(p, END_TIME.getPreferredName()), + END_TIME, ObjectParser.ValueType.VALUE); + PARSER.declareString(ConstructingObjectParser.constructorArg(), Calendar.ID); + PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), EVENT_ID); + } + + private final String description; + private final Date startTime; + private final Date endTime; + private final String calendarId; + private final String eventId; + + ScheduledEvent(String description, Date startTime, Date endTime, String calendarId, @Nullable String eventId) { + this.description = Objects.requireNonNull(description); + this.startTime = Objects.requireNonNull(startTime); + this.endTime = Objects.requireNonNull(endTime); + this.calendarId = Objects.requireNonNull(calendarId); + this.eventId = eventId; + } + + public String getDescription() { + return description; + } + + public Date getStartTime() { + return startTime; + } + + public Date getEndTime() { + return endTime; + } + + public String getCalendarId() { + return calendarId; + } + + public String getEventId() { + return eventId; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(DESCRIPTION.getPreferredName(), description); + builder.timeField(START_TIME.getPreferredName(), START_TIME.getPreferredName() + "_string", startTime.getTime()); + builder.timeField(END_TIME.getPreferredName(), END_TIME.getPreferredName() + "_string", endTime.getTime()); + builder.field(Calendar.ID.getPreferredName(), calendarId); + if (eventId != null) { + builder.field(EVENT_ID.getPreferredName(), eventId); + } + builder.endObject(); + return builder; + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + ScheduledEvent other = (ScheduledEvent) obj; + return Objects.equals(this.description, other.description) + && Objects.equals(this.startTime, other.startTime) + && Objects.equals(this.endTime, other.endTime) + && Objects.equals(this.calendarId, other.calendarId); + } + + @Override + public int hashCode() { + return Objects.hash(description, startTime, endTime, calendarId); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java index 547bc2e9a93..e0a9640ef40 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MLRequestConvertersTests.java @@ -38,9 +38,12 @@ import org.elasticsearch.client.ml.GetOverallBucketsRequest; import org.elasticsearch.client.ml.GetRecordsRequest; import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.PostDataRequest; +import org.elasticsearch.client.ml.PutCalendarRequest; import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.calendars.Calendar; +import org.elasticsearch.client.ml.calendars.CalendarTests; import org.elasticsearch.client.ml.datafeed.DatafeedConfig; import org.elasticsearch.client.ml.datafeed.DatafeedConfigTests; import org.elasticsearch.client.ml.job.config.AnalysisConfig; @@ -383,6 +386,17 @@ public class MLRequestConvertersTests extends ESTestCase { } } + public void testPutCalendar() throws IOException { + PutCalendarRequest putCalendarRequest = new PutCalendarRequest(CalendarTests.testInstance()); + Request request = MLRequestConverters.putCalendar(putCalendarRequest); + assertEquals(HttpPut.METHOD_NAME, request.getMethod()); + assertEquals("/_xpack/ml/calendars/" + putCalendarRequest.getCalendar().getId(), request.getEndpoint()); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, request.getEntity().getContent())) { + Calendar parsedCalendar = Calendar.PARSER.apply(parser, null); + assertThat(parsedCalendar, equalTo(putCalendarRequest.getCalendar())); + } + } + private static Job createValidJob(String jobId) { AnalysisConfig.Builder analysisConfig = AnalysisConfig.builder(Collections.singletonList( Detector.builder().setFunction("count").build())); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java index a07b4414843..598f29eec92 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/MachineLearningIT.java @@ -40,11 +40,15 @@ import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; import org.elasticsearch.client.ml.PostDataRequest; import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutCalendarRequest; +import org.elasticsearch.client.ml.PutCalendarResponse; import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.PutJobResponse; import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.calendars.Calendar; +import org.elasticsearch.client.ml.calendars.CalendarTests; import org.elasticsearch.client.ml.datafeed.DatafeedConfig; import org.elasticsearch.client.ml.job.config.AnalysisConfig; import org.elasticsearch.client.ml.job.config.DataDescription; @@ -397,6 +401,16 @@ public class MachineLearningIT extends ESRestHighLevelClientTestCase { return getResponse.isExists(); } + public void testPutCalendar() throws IOException { + + Calendar calendar = CalendarTests.testInstance(); + MachineLearningClient machineLearningClient = highLevelClient().machineLearning(); + PutCalendarResponse putCalendarResponse = execute(new PutCalendarRequest(calendar), machineLearningClient::putCalendar, + machineLearningClient::putCalendarAsync); + + assertThat(putCalendarResponse.getCalendar(), equalTo(calendar)); + } + public static String randomValidJobId() { CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz0123456789".toCharArray()); return generator.ofCodePointsLength(random(), 10, 10); diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java index 09d32710eb1..66a38ca781c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/MlClientDocumentationIT.java @@ -59,11 +59,14 @@ import org.elasticsearch.client.ml.OpenJobRequest; import org.elasticsearch.client.ml.OpenJobResponse; import org.elasticsearch.client.ml.PostDataRequest; import org.elasticsearch.client.ml.PostDataResponse; +import org.elasticsearch.client.ml.PutCalendarRequest; +import org.elasticsearch.client.ml.PutCalendarResponse; import org.elasticsearch.client.ml.PutDatafeedRequest; import org.elasticsearch.client.ml.PutDatafeedResponse; import org.elasticsearch.client.ml.PutJobRequest; import org.elasticsearch.client.ml.PutJobResponse; import org.elasticsearch.client.ml.UpdateJobRequest; +import org.elasticsearch.client.ml.calendars.Calendar; import org.elasticsearch.client.ml.datafeed.ChunkingConfig; import org.elasticsearch.client.ml.datafeed.DatafeedConfig; import org.elasticsearch.client.ml.job.config.AnalysisConfig; @@ -1368,7 +1371,7 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { IndexRequest indexRequest = new IndexRequest(".ml-anomalies-shared", "doc"); indexRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.IMMEDIATE); indexRequest.source("{\"job_id\": \"test-get-categories\", \"category_id\": 1, \"terms\": \"AAL\"," + - " \"regex\": \".*?AAL.*\", \"max_matching_length\": 3, \"examples\": [\"AAL\"]}", XContentType.JSON); + " \"regex\": \".*?AAL.*\", \"max_matching_length\": 3, \"examples\": [\"AAL\"]}", XContentType.JSON); client.index(indexRequest, RequestOptions.DEFAULT); { @@ -1402,17 +1405,17 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { // tag::x-pack-ml-get-categories-listener ActionListener listener = - new ActionListener() { - @Override - public void onResponse(GetCategoriesResponse getcategoriesResponse) { - // <1> - } + new ActionListener() { + @Override + public void onResponse(GetCategoriesResponse getcategoriesResponse) { + // <1> + } - @Override - public void onFailure(Exception e) { - // <2> - } - }; + @Override + public void onFailure(Exception e) { + // <2> + } + }; // end::x-pack-ml-get-categories-listener // Replace the empty listener by a blocking listener in test @@ -1424,6 +1427,48 @@ public class MlClientDocumentationIT extends ESRestHighLevelClientTestCase { // end::x-pack-ml-get-categories-execute-async assertTrue(latch.await(30L, TimeUnit.SECONDS)); - } + } + } + + public void testPutCalendar() throws IOException, InterruptedException { + RestHighLevelClient client = highLevelClient(); + + //tag::x-pack-ml-put-calendar-request + Calendar calendar = new Calendar("public_holidays", Collections.singletonList("job_1"), "A calendar for public holidays"); + PutCalendarRequest request = new PutCalendarRequest(calendar); // <1> + //end::x-pack-ml-put-calendar-request + + //tag::x-pack-ml-put-calendar-execution + PutCalendarResponse response = client.machineLearning().putCalendar(request, RequestOptions.DEFAULT); + //end::x-pack-ml-put-calendar-execution + + //tag::x-pack-ml-put-calendar-response + Calendar newCalendar = response.getCalendar(); // <1> + //end::x-pack-ml-put-calendar-response + assertThat(newCalendar.getId(), equalTo("public_holidays")); + + // tag::x-pack-ml-put-calendar-listener + ActionListener listener = new ActionListener() { + @Override + public void onResponse(PutCalendarResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + } + }; + // end::x-pack-ml-put-calendar-listener + + // Replace the empty listener by a blocking listener in test + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::x-pack-ml-put-calendar-execute-async + client.machineLearning().putCalendarAsync(request, RequestOptions.DEFAULT, listener); // <1> + // end::x-pack-ml-put-calendar-execute-async + + assertTrue(latch.await(30L, TimeUnit.SECONDS)); } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarRequestTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarRequestTests.java new file mode 100644 index 00000000000..3b5b6b8658b --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarRequestTests.java @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.calendars.Calendar; +import org.elasticsearch.client.ml.calendars.CalendarTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class PutCalendarRequestTests extends AbstractXContentTestCase { + @Override + protected PutCalendarRequest createTestInstance() { + return new PutCalendarRequest(CalendarTests.testInstance()); + } + + @Override + protected PutCalendarRequest doParseInstance(XContentParser parser) throws IOException { + return new PutCalendarRequest(Calendar.PARSER.apply(parser, null)); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarResponseTests.java new file mode 100644 index 00000000000..1756e1ceb7f --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/PutCalendarResponseTests.java @@ -0,0 +1,43 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml; + +import org.elasticsearch.client.ml.calendars.CalendarTests; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; + +public class PutCalendarResponseTests extends AbstractXContentTestCase { + @Override + protected PutCalendarResponse createTestInstance() { + return new PutCalendarResponse(CalendarTests.testInstance()); + } + + @Override + protected PutCalendarResponse doParseInstance(XContentParser parser) throws IOException { + return PutCalendarResponse.fromXContent(parser); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/CalendarTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/CalendarTests.java new file mode 100644 index 00000000000..ddb5c7be0b9 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/CalendarTests.java @@ -0,0 +1,61 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml.calendars; + +import com.carrotsearch.randomizedtesting.generators.CodepointSetGenerator; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class CalendarTests extends AbstractXContentTestCase { + + public static Calendar testInstance() { + int size = randomInt(10); + List items = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + items.add(randomAlphaOfLengthBetween(1, 20)); + } + String description = null; + if (randomBoolean()) { + description = randomAlphaOfLength(20); + } + + CodepointSetGenerator generator = new CodepointSetGenerator("abcdefghijklmnopqrstuvwxyz".toCharArray()); + return new Calendar(generator.ofCodePointsLength(random(), 10, 10), items, description); + } + + @Override + protected Calendar createTestInstance() { + return testInstance(); + } + + @Override + protected Calendar doParseInstance(XContentParser parser) throws IOException { + return Calendar.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/ScheduledEventTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/ScheduledEventTests.java new file mode 100644 index 00000000000..0b7a2933402 --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/ml/calendars/ScheduledEventTests.java @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch 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 org.elasticsearch.client.ml.calendars; + +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; + +import java.util.Date; + +public class ScheduledEventTests extends AbstractXContentTestCase { + + public static ScheduledEvent testInstance() { + Date start = new Date(randomNonNegativeLong()); + Date end = new Date(start.getTime() + randomIntBetween(1, 10000) * 1000); + + return new ScheduledEvent(randomAlphaOfLength(10), start, end, randomAlphaOfLengthBetween(1, 20), + randomBoolean() ? null : randomAlphaOfLength(7)); + } + + @Override + protected ScheduledEvent createTestInstance() { + return testInstance(); + } + + @Override + protected ScheduledEvent doParseInstance(XContentParser parser) { + return ScheduledEvent.PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } +} diff --git a/docs/java-rest/high-level/ml/get-buckets.asciidoc b/docs/java-rest/high-level/ml/get-buckets.asciidoc index 33a3059166c..f77b368a495 100644 --- a/docs/java-rest/high-level/ml/get-buckets.asciidoc +++ b/docs/java-rest/high-level/ml/get-buckets.asciidoc @@ -112,7 +112,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-buckets-l <1> `onResponse` is called back when the action is completed successfully <2> `onFailure` is called back when some unexpected error occurs -[[java-rest-high-snapshot-ml-get-buckets-response]] +[[java-rest-high-x-pack-ml-get-buckets-response]] ==== Get Buckets Response The returned `GetBucketsResponse` contains the requested buckets: diff --git a/docs/java-rest/high-level/ml/get-categories.asciidoc b/docs/java-rest/high-level/ml/get-categories.asciidoc index 0e86a2b7f33..2a5988a5cc6 100644 --- a/docs/java-rest/high-level/ml/get-categories.asciidoc +++ b/docs/java-rest/high-level/ml/get-categories.asciidoc @@ -70,7 +70,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-categorie <1> `onResponse` is called back when the action is completed successfully <2> `onFailure` is called back when some unexpected error occurs -[[java-rest-high-snapshot-ml-get-categories-response]] +[[java-rest-high-x-pack-ml-get-categories-response]] ==== Get Categories Response The returned `GetCategoriesResponse` contains the requested categories: diff --git a/docs/java-rest/high-level/ml/get-influencers.asciidoc b/docs/java-rest/high-level/ml/get-influencers.asciidoc index e53e92ff1df..8b1ba061bcb 100644 --- a/docs/java-rest/high-level/ml/get-influencers.asciidoc +++ b/docs/java-rest/high-level/ml/get-influencers.asciidoc @@ -99,7 +99,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-influence <1> `onResponse` is called back when the action is completed successfully <2> `onFailure` is called back when some unexpected error occurs -[[java-rest-high-snapshot-ml-get-influencers-response]] +[[java-rest-high-x-pack-ml-get-influencers-response]] ==== Get Influencers Response The returned `GetInfluencersResponse` contains the requested influencers: diff --git a/docs/java-rest/high-level/ml/get-overall-buckets.asciidoc b/docs/java-rest/high-level/ml/get-overall-buckets.asciidoc index 832eb8f2514..1e299dc2721 100644 --- a/docs/java-rest/high-level/ml/get-overall-buckets.asciidoc +++ b/docs/java-rest/high-level/ml/get-overall-buckets.asciidoc @@ -94,7 +94,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-overall-b <1> `onResponse` is called back when the action is completed successfully <2> `onFailure` is called back when some unexpected error occurs -[[java-rest-high-snapshot-ml-get-overall-buckets-response]] +[[java-rest-high-x-pack-ml-get-overall-buckets-response]] ==== Get Overall Buckets Response The returned `GetOverallBucketsResponse` contains the requested buckets: diff --git a/docs/java-rest/high-level/ml/get-records.asciidoc b/docs/java-rest/high-level/ml/get-records.asciidoc index f8a88f34d33..7b8e6b13131 100644 --- a/docs/java-rest/high-level/ml/get-records.asciidoc +++ b/docs/java-rest/high-level/ml/get-records.asciidoc @@ -100,7 +100,7 @@ include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-get-records-l <1> `onResponse` is called back when the action is completed successfully <2> `onFailure` is called back when some unexpected error occurs -[[java-rest-high-snapshot-ml-get-records-response]] +[[java-rest-high-x-pack-ml-get-records-response]] ==== Get Records Response The returned `GetRecordsResponse` contains the requested records: diff --git a/docs/java-rest/high-level/ml/put-calendar.asciidoc b/docs/java-rest/high-level/ml/put-calendar.asciidoc new file mode 100644 index 00000000000..e6814c76fad --- /dev/null +++ b/docs/java-rest/high-level/ml/put-calendar.asciidoc @@ -0,0 +1,65 @@ +[[java-rest-high-x-pack-ml-put-calendar]] +=== Put Calendar API +Creates a new {ml} calendar. +The API accepts a `PutCalendarRequest` and responds +with a `PutCalendarResponse` object. + +[[java-rest-high-x-pack-ml-get-calendars-request]] +==== Put Calendar Request + +A `PutCalendarRequest` is constructed with a Calendar object + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-calendar-request] +-------------------------------------------------- +<1> Create a request with the given Calendar + + +[[java-rest-high-x-pack-ml-put-calendar-response]] +==== Put Calendar Response + +The returned `PutCalendarResponse` contains the created Calendar: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-calendar-response] +-------------------------------------------------- +<1> The created Calendar + +[[java-rest-high-x-pack-ml-put-calendar-execution]] +==== Execution +The request can be executed through the `MachineLearningClient` contained +in the `RestHighLevelClient` object, accessed via the `machineLearningClient()` method. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-calendar-execute] +-------------------------------------------------- + +[[java-rest-high-x-pack-ml-put-calendar-execution-async]] +==== Asynchronous Execution + +The request can also be executed asynchronously: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-calendar-execute-async] +-------------------------------------------------- +<1> The `PutCalendarResquest` to execute and the `ActionListener` to use when +the execution completes + +The asynchronous method does not block and returns immediately. Once it is +completed the `ActionListener` is called back with the `onResponse` method +if the execution is successful or the `onFailure` method if the execution +failed. + +A typical listener for `PutCalendarResponse` looks like: + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests}/MlClientDocumentationIT.java[x-pack-ml-put-calendar-listener] +-------------------------------------------------- +<1> `onResponse` is called back when the action is completed successfully +<2> `onFailure` is called back when some unexpected error occurs + diff --git a/docs/java-rest/high-level/supported-apis.asciidoc b/docs/java-rest/high-level/supported-apis.asciidoc index cb297d0f712..089fe26cede 100644 --- a/docs/java-rest/high-level/supported-apis.asciidoc +++ b/docs/java-rest/high-level/supported-apis.asciidoc @@ -230,6 +230,7 @@ The Java High Level REST Client supports the following Machine Learning APIs: * <> * <> * <> +* <> include::ml/put-job.asciidoc[] include::ml/get-job.asciidoc[] @@ -249,6 +250,7 @@ include::ml/get-records.asciidoc[] include::ml/post-data.asciidoc[] include::ml/get-influencers.asciidoc[] include::ml/get-categories.asciidoc[] +include::ml/put-calendar.asciidoc[] == Migration APIs From 82a6ae1daed4f95e3841f5e40cf846c039780ca0 Mon Sep 17 00:00:00 2001 From: Martijn van Groningen Date: Fri, 14 Sep 2018 17:18:00 +0200 Subject: [PATCH 42/45] [CCR] Move ccr tests in core module back to ccr module (#33711) When developing ccr it is not ideal if tests are in multiple modules. Even the classes these tests test are in the core module, it is easier if these tests are in ccr module in order to avoid running the test task in core module. This results in running many non ccr tests. This way when developing ccr we can run locally: ./gradlew x-pack:plugin:core:precommit x-pack:plugin:ccr:check before pushing to PR branches and be confident that the PR build passes, without running x-pack:plugin:core:check task. --- .../org/elasticsearch/xpack}/ccr/AutoFollowMetadataTests.java | 3 ++- .../xpack}/ccr/action/DeleteAutoFollowPatternRequestTests.java | 3 ++- .../xpack}/ccr/action/PutAutoFollowPatternRequestTests.java | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) rename x-pack/plugin/{core/src/test/java/org/elasticsearch/xpack/core => ccr/src/test/java/org/elasticsearch/xpack}/ccr/AutoFollowMetadataTests.java (95%) rename x-pack/plugin/{core/src/test/java/org/elasticsearch/xpack/core => ccr/src/test/java/org/elasticsearch/xpack}/ccr/action/DeleteAutoFollowPatternRequestTests.java (87%) rename x-pack/plugin/{core/src/test/java/org/elasticsearch/xpack/core => ccr/src/test/java/org/elasticsearch/xpack}/ccr/action/PutAutoFollowPatternRequestTests.java (97%) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadataTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java similarity index 95% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadataTests.java rename to x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java index 5227c04962a..cc617abc385 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/AutoFollowMetadataTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/AutoFollowMetadataTests.java @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.core.ccr; +package org.elasticsearch.xpack.ccr; import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractSerializingTestCase; +import org.elasticsearch.xpack.core.ccr.AutoFollowMetadata; import java.io.IOException; import java.util.Arrays; diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java similarity index 87% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java rename to x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java index 135e699bb35..251f99800f7 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/DeleteAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/DeleteAutoFollowPatternRequestTests.java @@ -3,9 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.core.ccr.action; +package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.test.AbstractStreamableTestCase; +import org.elasticsearch.xpack.core.ccr.action.DeleteAutoFollowPatternAction; public class DeleteAutoFollowPatternRequestTests extends AbstractStreamableTestCase { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java similarity index 97% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java rename to x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java index ced49bbae12..d3688c1136c 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/ccr/action/PutAutoFollowPatternRequestTests.java +++ b/x-pack/plugin/ccr/src/test/java/org/elasticsearch/xpack/ccr/action/PutAutoFollowPatternRequestTests.java @@ -3,12 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.core.ccr.action; +package org.elasticsearch.xpack.ccr.action; import org.elasticsearch.action.ActionRequestValidationException; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractStreamableXContentTestCase; +import org.elasticsearch.xpack.core.ccr.action.PutAutoFollowPatternAction; import java.io.IOException; import java.util.Arrays; From a0f0d7860ec4cf9881146b143fc034cef10c2fbd Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 14 Sep 2018 14:45:58 -0400 Subject: [PATCH 43/45] Cleanup assertions in global checkpoint listeners (#33722) This commit is a cleanup of the assertions in global checkpoint listeners, simplifying them and adding some messages to them in case the assertions trip. --- .../shard/GlobalCheckpointListeners.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java index bedd1654449..eb9e36eeec0 100644 --- a/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java +++ b/server/src/main/java/org/elasticsearch/index/shard/GlobalCheckpointListeners.java @@ -21,6 +21,7 @@ package org.elasticsearch.index.shard; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.Assertions; import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.util.concurrent.FutureUtils; @@ -150,6 +151,9 @@ public class GlobalCheckpointListeners implements Closeable { @Override public synchronized void close() throws IOException { + if (closed) { + assert listeners.isEmpty() : listeners; + } closed = true; notifyListeners(UNASSIGNED_SEQ_NO, new IndexShardClosedException(shardId)); } @@ -188,8 +192,8 @@ public class GlobalCheckpointListeners implements Closeable { } private void notifyListeners(final long globalCheckpoint, final IndexShardClosedException e) { - assert Thread.holdsLock(this); - assert (globalCheckpoint == UNASSIGNED_SEQ_NO && e != null) || (globalCheckpoint >= NO_OPS_PERFORMED && e == null); + assert Thread.holdsLock(this) : Thread.currentThread(); + assertNotification(globalCheckpoint, e); final Map>> listenersToNotify; if (globalCheckpoint != UNASSIGNED_SEQ_NO) { @@ -219,6 +223,8 @@ public class GlobalCheckpointListeners implements Closeable { } private void notifyListener(final GlobalCheckpointListener listener, final long globalCheckpoint, final Exception e) { + assertNotification(globalCheckpoint, e); + try { listener.accept(globalCheckpoint, e); } catch (final Exception caught) { @@ -231,10 +237,21 @@ public class GlobalCheckpointListeners implements Closeable { } else if (e instanceof IndexShardClosedException) { logger.warn("error notifying global checkpoint listener of closed shard", caught); } else { - assert e instanceof TimeoutException : e; logger.warn("error notifying global checkpoint listener of timeout", caught); } } } + private void assertNotification(final long globalCheckpoint, final Exception e) { + if (Assertions.ENABLED) { + assert globalCheckpoint >= UNASSIGNED_SEQ_NO : globalCheckpoint; + if (globalCheckpoint != UNASSIGNED_SEQ_NO) { + assert e == null : e; + } else { + assert e != null; + assert e instanceof IndexShardClosedException || e instanceof TimeoutException : e; + } + } + } + } From 9706584836f19b1666da2d86fb12b425c3a667ed Mon Sep 17 00:00:00 2001 From: Lisa Cawley Date: Fri, 14 Sep 2018 13:09:47 -0700 Subject: [PATCH 44/45] [DOCS] Moves security reference to docs folder (#33643) --- .../en => docs/reference}/security/reference/files.asciidoc | 1 + x-pack/docs/en/security/reference.asciidoc | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) rename {x-pack/docs/en => docs/reference}/security/reference/files.asciidoc (99%) diff --git a/x-pack/docs/en/security/reference/files.asciidoc b/docs/reference/security/reference/files.asciidoc similarity index 99% rename from x-pack/docs/en/security/reference/files.asciidoc rename to docs/reference/security/reference/files.asciidoc index ac4cc0aa201..64a004c6646 100644 --- a/x-pack/docs/en/security/reference/files.asciidoc +++ b/docs/reference/security/reference/files.asciidoc @@ -1,3 +1,4 @@ +[role="xpack"] [[security-files]] === Security Files diff --git a/x-pack/docs/en/security/reference.asciidoc b/x-pack/docs/en/security/reference.asciidoc index ba770c15232..75de1daee6d 100644 --- a/x-pack/docs/en/security/reference.asciidoc +++ b/x-pack/docs/en/security/reference.asciidoc @@ -7,5 +7,5 @@ * {ref}/security-api.html[Security API] * {ref}/xpack-commands.html[Security Commands] -:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/x-pack/docs/en/security/reference/files.asciidoc -include::reference/files.asciidoc[] +:edit_url: https://github.com/elastic/elasticsearch/edit/{branch}/docs/reference/security/reference/files.asciidoc +include::{es-repo-dir}/security/reference/files.asciidoc[] From 92a48fa6b84a331f966dec800c9e44234f7a2b73 Mon Sep 17 00:00:00 2001 From: Jason Tedor Date: Fri, 14 Sep 2018 16:14:03 -0400 Subject: [PATCH 45/45] Add script to cache dependencies (#33726) With the introduction of immutable workers into our CI, we now have a problem where we would have to download dependencies on every single build. To this end our infrastructure team has introduced the possibility to download dependencies one time and then bake these into the base immutable worker image. For this, we introduce our version of this special script which runs a task that downloads all dependencies of all configurations. With this script, our infrastructure team will be rebuilding these images on an at-least daily basis. This helps us avoid having to download the dependencies for every single build. --- .ci/packer_cache.sh | 19 +++++++++++++++++++ build.gradle | 8 ++++++++ 2 files changed, 27 insertions(+) create mode 100755 .ci/packer_cache.sh diff --git a/.ci/packer_cache.sh b/.ci/packer_cache.sh new file mode 100755 index 00000000000..906b2dd6420 --- /dev/null +++ b/.ci/packer_cache.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +SCRIPT="$0" + +# SCRIPT might be an arbitrarily deep series of symbolic links; loop until we +# have the concrete path +while [ -h "$SCRIPT" ] ; do + ls=$(ls -ld "$SCRIPT") + # Drop everything prior to -> + link=$(expr "$ls" : '.*-> \(.*\)$') + if expr "$link" : '/.*' > /dev/null; then + SCRIPT="$link" + else + SCRIPT=$(dirname "$SCRIPT")/"$link" + fi +done + +source $(dirname "${SCRIPT}")/java-versions.properties +JAVA_HOME="${HOME}"/.java/${ES_BUILD_JAVA} ./gradlew resolveAllDependencies --parallel diff --git a/build.gradle b/build.gradle index a0c00db10dc..a2b79d31bad 100644 --- a/build.gradle +++ b/build.gradle @@ -628,3 +628,11 @@ if (System.properties.get("build.compare") != null) { } } } + +allprojects { + task resolveAllDependencies { + doLast { + configurations.findAll { it.isCanBeResolved() }.each { it.resolve() } + } + } +}

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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/squirell-6-alias-props.png b/docs/reference/images/sql/client-apps/squirell-6-alias-props.png new file mode 100644 index 0000000000000000000000000000000000000000..a43e5b5be69275de2bdad06e30bc394ef978950c GIT binary patch literal 13045 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png b/docs/reference/images/sql/client-apps/workbench-1-manage-drivers.png new file mode 100644 index 0000000000000000000000000000000000000000..e305fd2a9dda239e6ca88d8e3ebd6d1279b96f21 GIT binary patch literal 16439 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-2-add-driver.png b/docs/reference/images/sql/client-apps/workbench-2-add-driver.png new file mode 100644 index 0000000000000000000000000000000000000000..03e740f400ae10fa13ec39d458cc936a5ca3e08f GIT binary patch literal 26008 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 literal 0 HcmV?d00001 diff --git a/docs/reference/images/sql/client-apps/workbench-3-connection.png b/docs/reference/images/sql/client-apps/workbench-3-connection.png new file mode 100644 index 0000000000000000000000000000000000000000..32643375e3de9f483e377feb6b54c63b1766646f GIT binary patch literal 35138 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