-
Notifications
You must be signed in to change notification settings - Fork 25.3k
Record SLM history into an index #41707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a268c95
e5e61d3
05b9512
bd96c6f
0bde4fc
bed7945
242f478
0e7847b
afb3028
f5e6233
24a468e
1d77840
db674c6
483cd51
38deb63
ea8b159
b4e124c
b25dd03
64cbe40
e16d3f3
422daab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,223 @@ | ||
/* | ||
* 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.core.snapshotlifecycle.history; | ||
|
||
import org.elasticsearch.ElasticsearchException; | ||
import org.elasticsearch.common.Nullable; | ||
import org.elasticsearch.common.ParseField; | ||
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.bytes.BytesReference; | ||
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.xcontent.ConstructingObjectParser; | ||
import org.elasticsearch.common.xcontent.ToXContent; | ||
import org.elasticsearch.common.xcontent.ToXContentObject; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.XContentParser; | ||
import org.elasticsearch.common.xcontent.json.JsonXContent; | ||
import org.elasticsearch.xpack.core.snapshotlifecycle.SnapshotLifecyclePolicy; | ||
|
||
import java.io.IOException; | ||
import java.util.Collections; | ||
import java.util.Map; | ||
import java.util.Objects; | ||
|
||
import static org.elasticsearch.ElasticsearchException.REST_EXCEPTION_SKIP_STACK_TRACE; | ||
|
||
/** | ||
* Represents the record of a Snapshot Lifecycle Management action, so that it | ||
* can be indexed in a history index or recorded to a log in a structured way | ||
*/ | ||
public class SnapshotHistoryItem implements Writeable, ToXContentObject { | ||
static final ParseField TIMESTAMP = new ParseField("@timestamp"); | ||
static final ParseField POLICY_ID = new ParseField("policy"); | ||
static final ParseField REPOSITORY = new ParseField("repository"); | ||
static final ParseField SNAPSHOT_NAME = new ParseField("snapshot_name"); | ||
static final ParseField OPERATION = new ParseField("operation"); | ||
static final ParseField SUCCESS = new ParseField("success"); | ||
private static final String CREATE_OPERATION = "CREATE"; | ||
protected final long timestamp; | ||
protected final String policyId; | ||
protected final String repository; | ||
protected final String snapshotName; | ||
protected final String operation; | ||
protected final boolean success; | ||
|
||
private final Map<String, Object> snapshotConfiguration; | ||
@Nullable | ||
private final String errorDetails; | ||
|
||
static final ParseField SNAPSHOT_CONFIG = new ParseField("configuration"); | ||
static final ParseField ERROR_DETAILS = new ParseField("error_details"); | ||
|
||
@SuppressWarnings("unchecked") | ||
private static final ConstructingObjectParser<SnapshotHistoryItem, String> PARSER = | ||
new ConstructingObjectParser<>("snapshot_lifecycle_history_item", true, | ||
(a, id) -> { | ||
final long timestamp = (long) a[0]; | ||
final String policyId = (String) a[1]; | ||
final String repository = (String) a[2]; | ||
final String snapshotName = (String) a[3]; | ||
final String operation = (String) a[4]; | ||
final boolean success = (boolean) a[5]; | ||
final Map<String, Object> snapshotConfiguration = (Map<String, Object>) a[6]; | ||
final String errorDetails = (String) a[7]; | ||
return new SnapshotHistoryItem(timestamp, policyId, repository, snapshotName, operation, success, | ||
snapshotConfiguration, errorDetails); | ||
}); | ||
|
||
static { | ||
PARSER.declareLong(ConstructingObjectParser.constructorArg(), TIMESTAMP); | ||
PARSER.declareString(ConstructingObjectParser.constructorArg(), POLICY_ID); | ||
PARSER.declareString(ConstructingObjectParser.constructorArg(), REPOSITORY); | ||
PARSER.declareString(ConstructingObjectParser.constructorArg(), SNAPSHOT_NAME); | ||
PARSER.declareString(ConstructingObjectParser.constructorArg(), OPERATION); | ||
PARSER.declareBoolean(ConstructingObjectParser.constructorArg(), SUCCESS); | ||
PARSER.declareObject(ConstructingObjectParser.constructorArg(), (p, c) -> p.map(), SNAPSHOT_CONFIG); | ||
PARSER.declareStringOrNull(ConstructingObjectParser.constructorArg(), ERROR_DETAILS); | ||
} | ||
|
||
public static SnapshotHistoryItem parse(XContentParser parser, String name) { | ||
return PARSER.apply(parser, name); | ||
} | ||
|
||
SnapshotHistoryItem(long timestamp, String policyId, String repository, String snapshotName, String operation, | ||
boolean success, Map<String, Object> snapshotConfiguration, String errorDetails) { | ||
this.timestamp = timestamp; | ||
this.policyId = Objects.requireNonNull(policyId); | ||
this.repository = Objects.requireNonNull(repository); | ||
this.snapshotName = Objects.requireNonNull(snapshotName); | ||
this.operation = Objects.requireNonNull(operation); | ||
this.success = success; | ||
this.snapshotConfiguration = Objects.requireNonNull(snapshotConfiguration); | ||
this.errorDetails = errorDetails; | ||
} | ||
|
||
public static SnapshotHistoryItem successRecord(long timestamp, SnapshotLifecyclePolicy policy, String snapshotName) { | ||
return new SnapshotHistoryItem(timestamp, policy.getId(), policy.getRepository(), snapshotName, CREATE_OPERATION, true, | ||
policy.getConfig(), null); | ||
} | ||
|
||
public static SnapshotHistoryItem failureRecord(long timeStamp, SnapshotLifecyclePolicy policy, String snapshotName, | ||
Exception exception) throws IOException { | ||
ToXContent.Params stacktraceParams = new ToXContent.MapParams(Collections.singletonMap(REST_EXCEPTION_SKIP_STACK_TRACE, "false")); | ||
String exceptionString; | ||
try (XContentBuilder causeXContentBuilder = JsonXContent.contentBuilder()) { | ||
causeXContentBuilder.startObject(); | ||
ElasticsearchException.generateThrowableXContent(causeXContentBuilder, stacktraceParams, exception); | ||
causeXContentBuilder.endObject(); | ||
exceptionString = BytesReference.bytes(causeXContentBuilder).utf8ToString(); | ||
} | ||
return new SnapshotHistoryItem(timeStamp, policy.getId(), policy.getRepository(), snapshotName, CREATE_OPERATION, false, | ||
policy.getConfig(), exceptionString); | ||
} | ||
|
||
public SnapshotHistoryItem(StreamInput in) throws IOException { | ||
this.timestamp = in.readVLong(); | ||
this.policyId = in.readString(); | ||
this.repository = in.readString(); | ||
this.snapshotName = in.readString(); | ||
this.operation = in.readString(); | ||
this.success = in.readBoolean(); | ||
this.snapshotConfiguration = in.readMap(); | ||
this.errorDetails = in.readOptionalString(); | ||
} | ||
|
||
public Map<String, Object> getSnapshotConfiguration() { | ||
return snapshotConfiguration; | ||
} | ||
|
||
public String getErrorDetails() { | ||
return errorDetails; | ||
} | ||
|
||
public long getTimestamp() { | ||
return timestamp; | ||
} | ||
|
||
public String getPolicyId() { | ||
return policyId; | ||
} | ||
|
||
public String getRepository() { | ||
return repository; | ||
} | ||
|
||
public String getSnapshotName() { | ||
return snapshotName; | ||
} | ||
|
||
public String getOperation() { | ||
return operation; | ||
} | ||
|
||
public boolean isSuccess() { | ||
return success; | ||
} | ||
|
||
@Override | ||
public final void writeTo(StreamOutput out) throws IOException { | ||
out.writeVLong(timestamp); | ||
out.writeString(policyId); | ||
out.writeString(repository); | ||
out.writeString(snapshotName); | ||
out.writeString(operation); | ||
out.writeBoolean(success); | ||
out.writeMap(snapshotConfiguration); | ||
out.writeOptionalString(errorDetails); | ||
} | ||
|
||
@Override | ||
public final XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { | ||
builder.startObject(); | ||
{ | ||
builder.timeField(TIMESTAMP.getPreferredName(), "timestamp_string", timestamp); | ||
builder.field(POLICY_ID.getPreferredName(), policyId); | ||
builder.field(REPOSITORY.getPreferredName(), repository); | ||
builder.field(SNAPSHOT_NAME.getPreferredName(), snapshotName); | ||
builder.field(OPERATION.getPreferredName(), operation); | ||
builder.field(SUCCESS.getPreferredName(), success); | ||
builder.field(SNAPSHOT_CONFIG.getPreferredName(), snapshotConfiguration); | ||
builder.field(ERROR_DETAILS.getPreferredName(), errorDetails); | ||
} | ||
builder.endObject(); | ||
|
||
return builder; | ||
} | ||
|
||
@Override | ||
public boolean equals(Object o) { | ||
if (this == o) return true; | ||
if (o == null || getClass() != o.getClass()) return false; | ||
boolean result; | ||
if (this == o) result = true; | ||
if (o == null || getClass() != o.getClass()) result = false; | ||
SnapshotHistoryItem that1 = (SnapshotHistoryItem) o; | ||
result = isSuccess() == that1.isSuccess() && | ||
timestamp == that1.getTimestamp() && | ||
Objects.equals(getPolicyId(), that1.getPolicyId()) && | ||
Objects.equals(getRepository(), that1.getRepository()) && | ||
Objects.equals(getSnapshotName(), that1.getSnapshotName()) && | ||
Objects.equals(getOperation(), that1.getOperation()); | ||
if (!result) return false; | ||
SnapshotHistoryItem that = (SnapshotHistoryItem) o; | ||
return Objects.equals(getSnapshotConfiguration(), that.getSnapshotConfiguration()) && | ||
Objects.equals(getErrorDetails(), that.getErrorDetails()); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(getTimestamp(), getPolicyId(), getRepository(), getSnapshotName(), getOperation(), isSuccess(), | ||
getSnapshotConfiguration(), getErrorDetails()); | ||
} | ||
|
||
@Override | ||
public String toString() { | ||
return Strings.toString(this); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
/* | ||
* 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.core.snapshotlifecycle.history; | ||
|
||
import org.apache.logging.log4j.LogManager; | ||
import org.apache.logging.log4j.Logger; | ||
import org.apache.logging.log4j.message.ParameterizedMessage; | ||
import org.elasticsearch.action.ActionListener; | ||
import org.elasticsearch.action.index.IndexRequest; | ||
import org.elasticsearch.client.Client; | ||
import org.elasticsearch.common.settings.Settings; | ||
import org.elasticsearch.common.time.DateFormatter; | ||
import org.elasticsearch.common.xcontent.ToXContent; | ||
import org.elasticsearch.common.xcontent.XContentBuilder; | ||
import org.elasticsearch.common.xcontent.XContentFactory; | ||
|
||
import java.io.IOException; | ||
import java.time.Instant; | ||
import java.time.ZoneId; | ||
import java.time.ZonedDateTime; | ||
|
||
import static org.elasticsearch.xpack.core.indexlifecycle.LifecycleSettings.SLM_HISTORY_INDEX_ENABLED_SETTING; | ||
import static org.elasticsearch.xpack.core.snapshotlifecycle.history.SnapshotLifecycleTemplateRegistry.INDEX_TEMPLATE_VERSION; | ||
|
||
/** | ||
* Records Snapshot Lifecycle Management actions as represented by {@link SnapshotHistoryItem} into an index | ||
* for the purposes of querying and alerting. | ||
*/ | ||
public class SnapshotHistoryStore { | ||
private static final Logger logger = LogManager.getLogger(SnapshotHistoryStore.class); | ||
private static final DateFormatter indexTimeFormat = DateFormatter.forPattern("yyyy.MM"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is written to use monthly indices at the moment because it's likely that this index will have a very low volume of writes - I expect ~1/day to be common, and even the relatively high-volume usage of taking a snapshot every 30m would only produce a maximum of ~1500/month. Even 10 times that is still a small number of documents for one index. |
||
|
||
public static final String SLM_HISTORY_INDEX_PREFIX = ".slm-history-" + INDEX_TEMPLATE_VERSION + "-"; | ||
|
||
private final Client client; | ||
private final ZoneId timeZone; | ||
private final boolean slmHistoryEnabled; | ||
|
||
public SnapshotHistoryStore(Settings nodeSettings, Client client, ZoneId timeZone) { | ||
this.client = client; | ||
this.timeZone = timeZone; | ||
slmHistoryEnabled = SLM_HISTORY_INDEX_ENABLED_SETTING.get(nodeSettings); | ||
} | ||
|
||
/** | ||
* Attempts to asynchronously index a snapshot lifecycle management history entry | ||
* | ||
* @param item The entry to index | ||
*/ | ||
public void putAsync(SnapshotHistoryItem item) { | ||
if (slmHistoryEnabled == false) { | ||
logger.trace("not recording snapshot history item because [{}] is [false]: [{}]", | ||
SLM_HISTORY_INDEX_ENABLED_SETTING.getKey(), item); | ||
return; | ||
} | ||
final ZonedDateTime dateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(item.getTimestamp()), timeZone); | ||
final String index = getHistoryIndexNameForTime(dateTime); | ||
logger.trace("about to index snapshot history item in index [{}]: [{}]", index, item); | ||
try (XContentBuilder builder = XContentFactory.jsonBuilder()) { | ||
item.toXContent(builder, ToXContent.EMPTY_PARAMS); | ||
IndexRequest request = new IndexRequest(index) | ||
.source(builder); | ||
client.index(request, ActionListener.wrap(indexResponse -> { | ||
logger.debug("successfully indexed snapshot history item with id [{}] in index [{}]: [{}]", | ||
indexResponse.getId(), index, item); | ||
}, exception -> { | ||
logger.error(new ParameterizedMessage("failed to index snapshot history item in index [{}]: [{}]", | ||
index, item), exception); | ||
})); | ||
} catch (IOException exception) { | ||
logger.error(new ParameterizedMessage("failed to index snapshot history item in index [{}]: [{}]", | ||
index, item), exception); | ||
} | ||
} | ||
|
||
|
||
static String getHistoryIndexNameForTime(ZonedDateTime time) { | ||
return SLM_HISTORY_INDEX_PREFIX + indexTimeFormat.format(time); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this can't contain any sensitive information right ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only information the snapshot configuration will contain is:
include_global_state
,partial
,ignore_unavailable
, etc)So the only way this would contain sensitive information is if the user attaches it in the metadata, and there's not much we can do about that.