Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
3e77c52
Add repositories stats tracking API
fcofdez Aug 2, 2020
fc7ca20
Merge remote-tracking branch 'origin/master' into metering
fcofdez Aug 2, 2020
528da9a
Add docs
fcofdez Aug 3, 2020
fcc15c8
Merge remote-tracking branch 'origin/master' into metering
fcofdez Aug 3, 2020
f71fabb
Fix build scripts
fcofdez Aug 3, 2020
767edd6
Address first round of review comments
fcofdez Aug 4, 2020
20a1dfe
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 4, 2020
02c5d93
Simplify TransportActions and return repositories stats during archive
fcofdez Aug 5, 2020
a3ce634
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 5, 2020
158ad06
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 10, 2020
b4bb846
Keep track of cluster version on RepositoryStatsSnapshot.
fcofdez Aug 10, 2020
81b2bf7
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 10, 2020
d0a5995
Fix test
fcofdez Aug 11, 2020
278eaa7
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 11, 2020
d8aea38
Rename repositories-stats to repositories-metering
fcofdez Aug 11, 2020
7e4ab0c
Fix styleCheck
fcofdez Aug 11, 2020
47c18fa
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 18, 2020
d7020cb
Rename operation names on repositories metering
fcofdez Aug 18, 2020
ddc2cc1
Fix tests
fcofdez Aug 18, 2020
1f93514
Improve docs
fcofdez Aug 18, 2020
fdce53f
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 18, 2020
ee0c47b
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 20, 2020
2374fd1
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Aug 31, 2020
449069a
Address review comments
fcofdez Sep 2, 2020
413472a
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Sep 2, 2020
bc94df1
Address review comments
fcofdez Sep 3, 2020
e53322b
Use raw base_path for Repository metering API
fcofdez Sep 3, 2020
1b79828
minor corrections
fcofdez Sep 4, 2020
79cb0f1
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Sep 4, 2020
f9fa369
Minor correction
fcofdez Sep 4, 2020
c3a1f3a
Fix testRequestStats. Mention uploads tracked on GCS InsertObject.
fcofdez Sep 4, 2020
d142589
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Sep 4, 2020
bbc23c1
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Sep 7, 2020
a7b2ef8
Minor corrections
fcofdez Sep 7, 2020
b97f5f3
Merge remote-tracking branch 'origin/master' into metering-api-new
fcofdez Sep 7, 2020
6df2755
Merge branch 'master' into metering-api-new
elasticmachine Sep 7, 2020
3e2752b
Merge branch 'master' into metering-api-new
elasticmachine Sep 8, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[[clear-repositories-stats-archive-api]]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized :) Do we actually want to publicly document this API? If so I think we should make it very clear that this API is not supported/stable (regardless of what kind of stability we might offer here).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can state that this API is not stable and can suffer changes, I think it can be useful for consumers of this API to have the information at hand. But I'm not sure what's our policy here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For now, we can just put on the "experimental tag", and mark the license as basic.

Copy link
Contributor

@ywelsch ywelsch Aug 10, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can also add a header that this is an API meant to be used by Elastic's commercial offerings, to avoid any confusion.

=== Clear repositories statistics archive API
++++
<titleabbrev>Clear repositories statistics archive API</titleabbrev>
++++

Removes the archived repositories statistics in the cluster.

[[clear-repositories-stats-archive-api-request]]
==== {api-request-title}

`DELETE /_nodes/<node_id>/_repositories_stats`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After I had some time to think about this ... I wonder, why make life for users so hard by adding node_id here. Wouldn't it be much easier if we just allowed for passing a list of emphemeral_id for archived entries instead so that the consuming API can delete after having safely persisted an id avoiding essentially all kinds of possible races and making this much easier to use?
In the implementation we could simply broadcast the delete to all nodes so we don't have to figure out what id lives on what node I guess to keep it simple.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ywelsch maybe you have an opinion on this as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we think this API would be hard to use, maybe we can limit its scope to a single node? I added it as I needed a way to clear the archive between tests, if there's a simpler way that can simplify this I'm open to it 👍 .

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we can use a logical timestamp here to say "only clear repositories that I've observed", and use the cluster state version as such a logical timestamp. I think I would prefer that over a list of all IDs for archived entries. The API would still allow the ability to select nodes (in particular allow for _local).


[[clear-repositories-stats-archive-api-desc]]
==== {api-description-title}

You can clear the archived repositories utilization statistics using this API.

All the nodes selective options are explained <<cluster-nodes,here>>.

[[clear-repositories-stats-archive-api-path-params]]
==== {api-path-parms-title}

include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=node-id]

[[clear-repositories-stats-archive-api-query-params]]
==== {api-query-parms-title}

[role="child_attributes"]
[[clear-repositories-stats-archive-api-response-body]]
==== {api-response-body-title}
142 changes: 142 additions & 0 deletions docs/reference/repositories-stats/apis/get-repositories-stats.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
[[get-repositories-stats-api]]
=== Repositories statistics API
++++
<titleabbrev>Repositories statistics</titleabbrev>
++++

Returns cluster repositories utilization statistics.

[[get-repositories-stats-api-request]]
==== {api-request-title}

`GET /_nodes/<node_id>/_repositories_stats`

[[get-repositories-stats-api-desc]]
==== {api-description-title}

You can use the cluster repositories statistics API to retrieve repository utilization statistics in a cluster.


All the nodes selective options are explained <<cluster-nodes,here>>.

[[get-repositories-stats-api-path-params]]
==== {api-path-parms-title}

include::{es-repo-dir}/rest-api/common-parms.asciidoc[tag=node-id]

[[get-repositories-stats-api-query-params]]
==== {api-query-parms-title}

[role="child_attributes"]
[[get-repositories-stats-api-response-body]]
==== {api-response-body-title}

`_nodes`::
(object)
Contains statistics about the number of nodes selected by the request.
+
.Properties of `_nodes`
[%collapsible%open]
====
`total`::
(integer)
Total number of nodes selected by the request.

`successful`::
(integer)
Number of nodes that responded successfully to the request.

`failed`::
(integer)
Number of nodes that rejected the request or failed to respond. If this value
is not `0`, a reason for the rejection or failure is included in the response.
====

`cluster_name`::
(string)
Name of the cluster. Based on the <<cluster.name>> setting.

`nodes`::
(object)
Contains repository utilization statistics for the nodes selected by the request.
+
.Properties of `nodes`
[%collapsible%open]
====
`<node_id>`::
(array)
An array of repository utilization statistics for the node.
+
.Properties of objects in `node_id`
[%collapsible%open]
=====
`repository_name`::
(string)
Repository name.

`repository_type`::
(string)
Repository type.

`repository_location`::
(string)
Represents an unique location within the repository,
i.e. a `bucket` + `base_path`.

`repository_ephemeral_id`::
(string)
An identifier that changes every time the repository is updated.

`repository_started_at`::
(long)
Time the repository was created or updated. Recorded in milliseconds
since the https://en.wikipedia.org/wiki/Unix_time[Unix Epoch].

`repository_stopped_at`::
(long)
Time the repository was deleted or updated. Recorded in milliseconds
since the https://en.wikipedia.org/wiki/Unix_time[Unix Epoch].

`request_counts`::
(object)
An object with the number of request performed against the repository
grouped by request type.
+
.Properties of `request_counts` for repository type `Azure`
[%collapsible%open]
======
`HEAD`::
(long) Number of https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob-properties[Get Blob Properties] requests.
`GET`::
(long) Number of https://docs.microsoft.com/en-us/rest/api/storageservices/get-blob[Get Blob] requests.
`PUT`::
(long) Number of https://docs.microsoft.com/en-us/rest/api/storageservices/put-blob[Put Blob] requests.
======
+
.Properties of `request_counts` for repository type `GCP`
[%collapsible%open]
======
`GET`::
(long) Number of https://cloud.google.com/storage/docs/json_api/v1/objects/get[GET] requests.
`LIST`::
(long) Number of https://cloud.google.com/storage/docs/json_api/v1/objects/list[LIST] requests.
`PUT`::
(long) Number of https://cloud.google.com/storage/docs/performing-resumable-uploads[PUT] requests.
`POST`::
(long) Number of https://cloud.google.com/storage/docs/json_api/v1/objects/insert[POST] requests.
======
+
.Properties of `request_counts` for repository type `S3`
[%collapsible%open]
======
`GET`::
(long) Number of https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html[GET] requests.
`LIST`::
(long) Number of https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjects.html[LIST] requests.
`PUT`::
(long) Number of https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutObject.html[PUT] requests.
`POST`::
(long) Number of https://docs.aws.amazon.com/AmazonS3/latest/dev/mpuoverview.html[MultiPart] requests.
======
=====
====
12 changes: 12 additions & 0 deletions docs/reference/repositories-stats/apis/repositories-stats.asciidoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[[repositories-stats-apis]]
== Repositories utilization statistics APIs

experimental[]

You can use the following APIs to retrieve statistics about the repositories utilization.

* <<get-repositories-stats-api,Get repositories utilization statisticss>>
* <<clear-repositories-stats-archive-api,Clear repositories utilization statistics archive>>

include::get-repositories-stats.asciidoc[]
include::clear-repositories-stats-archive.asciidoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.indices.recovery.RecoverySettings;
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;
import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository;

import java.util.Locale;
import java.util.function.Function;
Expand All @@ -52,7 +52,7 @@
* <dt>{@code compress}</dt><dd>If set to true metadata files will be stored compressed. Defaults to false.</dd>
* </dl>
*/
public class AzureRepository extends BlobStoreRepository {
public class AzureRepository extends MeteredBlobStoreRepository {
private static final Logger logger = LogManager.getLogger(AzureRepository.class);

public static final String TYPE = "azure";
Expand Down Expand Up @@ -82,7 +82,12 @@ public AzureRepository(
final AzureStorageService storageService,
final ClusterService clusterService,
final RecoverySettings recoverySettings) {
super(metadata, namedXContentRegistry, clusterService, recoverySettings, buildBasePath(metadata));
super(metadata,
namedXContentRegistry,
clusterService,
recoverySettings,
buildBasePath(metadata),
Repository.CONTAINER_SETTING.get(metadata.settings()));
this.chunkSize = Repository.CHUNK_SIZE_SETTING.get(metadata.settings());
this.storageService = storageService;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,15 @@
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.indices.recovery.RecoverySettings;
import org.elasticsearch.repositories.RepositoryException;
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;
import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository;

import java.util.function.Function;

import static org.elasticsearch.common.settings.Setting.Property;
import static org.elasticsearch.common.settings.Setting.byteSizeSetting;
import static org.elasticsearch.common.settings.Setting.simpleString;

class GoogleCloudStorageRepository extends BlobStoreRepository {
class GoogleCloudStorageRepository extends MeteredBlobStoreRepository {
private static final Logger logger = LogManager.getLogger(GoogleCloudStorageRepository.class);

// package private for testing
Expand Down Expand Up @@ -72,7 +72,7 @@ class GoogleCloudStorageRepository extends BlobStoreRepository {
final GoogleCloudStorageService storageService,
final ClusterService clusterService,
final RecoverySettings recoverySettings) {
super(metadata, namedXContentRegistry, clusterService, recoverySettings, buildBasePath(metadata));
super(metadata, namedXContentRegistry, clusterService, recoverySettings, buildBasePath(metadata), getSetting(BUCKET, metadata));
this.storageService = storageService;

this.chunkSize = getSetting(CHUNK_SIZE, metadata);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
import org.elasticsearch.repositories.RepositoryData;
import org.elasticsearch.repositories.RepositoryException;
import org.elasticsearch.repositories.ShardGenerations;
import org.elasticsearch.repositories.blobstore.BlobStoreRepository;
import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository;
import org.elasticsearch.snapshots.SnapshotId;
import org.elasticsearch.snapshots.SnapshotInfo;
import org.elasticsearch.snapshots.SnapshotsService;
Expand All @@ -66,7 +66,7 @@
* <dt>{@code compress}</dt><dd>If set to true metadata files will be stored compressed. Defaults to false.</dd>
* </dl>
*/
class S3Repository extends BlobStoreRepository {
class S3Repository extends MeteredBlobStoreRepository {
private static final Logger logger = LogManager.getLogger(S3Repository.class);

static final String TYPE = "s3";
Expand Down Expand Up @@ -194,7 +194,12 @@ class S3Repository extends BlobStoreRepository {
final S3Service service,
final ClusterService clusterService,
final RecoverySettings recoverySettings) {
super(metadata, namedXContentRegistry, clusterService, recoverySettings, buildBasePath(metadata));
super(metadata,
namedXContentRegistry,
clusterService,
recoverySettings,
buildBasePath(metadata),
BUCKET_SETTING.get(metadata.settings()));
this.service = service;

// Parse and validate the user's S3 Storage Class setting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,12 @@
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.regex.Regex;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ConcurrentCollections;
import org.elasticsearch.core.internal.io.IOUtils;
import org.elasticsearch.repositories.blobstore.MeteredBlobStoreRepository;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;

Expand All @@ -56,6 +59,8 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* Service responsible for maintaining and providing access to snapshot repositories on nodes.
Expand All @@ -64,6 +69,12 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C

private static final Logger logger = LogManager.getLogger(RepositoriesService.class);

public static final Setting<TimeValue> REPOSITORIES_STATS_ARCHIVE_RETENTION_PERIOD =
Setting.positiveTimeSetting("repositories.stats.archive.retention_period", TimeValue.timeValueHours(2), Setting.Property.NodeScope);

public static final Setting<Integer> REPOSITORIES_STATS_ARCHIVE_MAX_ARCHIVED_STATS =
Setting.intSetting("repositories.stats.archive.max_archived_stats", 100, 0, Setting.Property.NodeScope);

private final Map<String, Repository.Factory> typesRegistry;
private final Map<String, Repository.Factory> internalTypesRegistry;

Expand All @@ -75,6 +86,7 @@ public class RepositoriesService extends AbstractLifecycleComponent implements C

private final Map<String, Repository> internalRepositories = ConcurrentCollections.newConcurrentMap();
private volatile Map<String, Repository> repositories = Collections.emptyMap();
private final RepositoriesStatsArchive repositoriesStatsArchive;

public RepositoriesService(Settings settings, ClusterService clusterService, TransportService transportService,
Map<String, Repository.Factory> typesRegistry, Map<String, Repository.Factory> internalTypesRegistry,
Expand All @@ -89,6 +101,10 @@ public RepositoriesService(Settings settings, ClusterService clusterService, Tra
clusterService.addHighPriorityApplier(this);
}
this.verifyAction = new VerifyNodeRepositoryAction(transportService, clusterService, this);
this.repositoriesStatsArchive = new RepositoriesStatsArchive(REPOSITORIES_STATS_ARCHIVE_RETENTION_PERIOD.get(settings),
REPOSITORIES_STATS_ARCHIVE_MAX_ARCHIVED_STATS.get(settings),
threadPool::relativeTimeInMillis,
threadPool::absoluteTimeInMillis);
}

/**
Expand Down Expand Up @@ -123,7 +139,7 @@ public void registerRepository(final PutRepositoryRequest request, final ActionL

// Trying to create the new repository on master to make sure it works
try {
closeRepository(createRepository(newRepositoryMetadata, typesRegistry));
closeRepository(createRepository(newRepositoryMetadata, typesRegistry), false);
} catch (Exception e) {
registrationListener.onFailure(e);
return;
Expand Down Expand Up @@ -397,6 +413,27 @@ public Repository repository(String repositoryName) {
throw new RepositoryMissingException(repositoryName);
}

public List<RepositoryStatsSnapshot> repositoriesStats() {
List<RepositoryStatsSnapshot> archivedRepoStats = repositoriesStatsArchive.getArchivedStats();
List<RepositoryStatsSnapshot> activeRepoStats = getRepositoryStatsForActiveRepositories();

List<RepositoryStatsSnapshot> repositoriesStats = new ArrayList<>(archivedRepoStats);
repositoriesStats.addAll(activeRepoStats);
return repositoriesStats;
}

private List<RepositoryStatsSnapshot> getRepositoryStatsForActiveRepositories() {
return Stream.concat(repositories.values().stream(), internalRepositories.values().stream())
.filter(r -> r instanceof MeteredBlobStoreRepository)
.map(r -> (MeteredBlobStoreRepository) r)
.map(MeteredBlobStoreRepository::statsSnapshot)
.collect(Collectors.toList());
}

public List<RepositoryStatsSnapshot> clearRepositoriesStatsArchive() {
return repositoriesStatsArchive.clear();
}

public void registerInternalRepository(String name, String type) {
RepositoryMetadata metadata = new RepositoryMetadata(name, type, Settings.EMPTY);
Repository repository = internalRepositories.computeIfAbsent(name, (n) -> {
Expand All @@ -423,8 +460,18 @@ public void unregisterInternalRepository(String name) {

/** Closes the given repository. */
private void closeRepository(Repository repository) {
closeRepository(repository, true);
}

private void closeRepository(Repository repository, boolean archiveStats) {
logger.debug("closing repository [{}][{}]", repository.getMetadata().type(), repository.getMetadata().name());
repository.close();
if (archiveStats && repository instanceof MeteredBlobStoreRepository) {
RepositoryStatsSnapshot stats = ((MeteredBlobStoreRepository) repository).statsSnapshot();
if (repositoriesStatsArchive.archive(stats) == false) {
logger.warn("Unable to archive the repository stats [{}] as the archive is full.", stats);
}
}
}

/**
Expand Down
Loading