Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -9,6 +9,7 @@

package org.elasticsearch.action.admin.cluster.stats;

import org.elasticsearch.TransportVersion;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.stats.NodeStats;
import org.elasticsearch.action.admin.indices.stats.ShardStats;
Expand All @@ -24,6 +25,8 @@

public class ClusterStatsNodeResponse extends BaseNodeResponse {

static final TransportVersion PROJECT_ROUTING_USAGE_STATS = TransportVersion.fromName("project_routing_usage_stats");

private final NodeInfo nodeInfo;
private final NodeStats nodeStats;
private final ShardStats[] shardsStats;
Expand All @@ -32,6 +35,7 @@ public class ClusterStatsNodeResponse extends BaseNodeResponse {
private final RepositoryUsageStats repositoryUsageStats;
private final CCSTelemetrySnapshot searchCcsMetrics;
private final CCSTelemetrySnapshot esqlCcsMetrics;
private final ProjectRoutingUsageSnapshot projectRoutingUsageSnapshot;

public ClusterStatsNodeResponse(StreamInput in) throws IOException {
super(in);
Expand All @@ -43,6 +47,9 @@ public ClusterStatsNodeResponse(StreamInput in) throws IOException {
repositoryUsageStats = RepositoryUsageStats.readFrom(in);
searchCcsMetrics = new CCSTelemetrySnapshot(in);
esqlCcsMetrics = new CCSTelemetrySnapshot(in);
projectRoutingUsageSnapshot = in.getTransportVersion().supports(PROJECT_ROUTING_USAGE_STATS)
? new ProjectRoutingUsageSnapshot(in)
: new ProjectRoutingUsageSnapshot();
}

public ClusterStatsNodeResponse(
Expand All @@ -54,7 +61,8 @@ public ClusterStatsNodeResponse(
SearchUsageStats searchUsageStats,
RepositoryUsageStats repositoryUsageStats,
CCSTelemetrySnapshot ccsTelemetrySnapshot,
CCSTelemetrySnapshot esqlTelemetrySnapshot
CCSTelemetrySnapshot esqlTelemetrySnapshot,
ProjectRoutingUsageSnapshot projectRoutingUsageSnapshot

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Do we need to ensure non-null here? I think we are going the other route with tags... I am not a huge fan of nulls, but here I am wondering if we have nothing to display (which would be the case for all non-CPS cases I imagine) why create the object at all?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See this comment: #155997 (comment)

) {
super(node);
this.nodeInfo = nodeInfo;
Expand All @@ -65,6 +73,7 @@ public ClusterStatsNodeResponse(
this.repositoryUsageStats = Objects.requireNonNull(repositoryUsageStats);
this.searchCcsMetrics = ccsTelemetrySnapshot;
this.esqlCcsMetrics = esqlTelemetrySnapshot;
this.projectRoutingUsageSnapshot = Objects.requireNonNull(projectRoutingUsageSnapshot);
}

public NodeInfo nodeInfo() {
Expand Down Expand Up @@ -103,6 +112,10 @@ public CCSTelemetrySnapshot getEsqlCcsMetrics() {
return esqlCcsMetrics;
}

public ProjectRoutingUsageSnapshot getProjectRoutingUsageSnapshot() {
return projectRoutingUsageSnapshot;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
Expand All @@ -114,6 +127,9 @@ public void writeTo(StreamOutput out) throws IOException {
repositoryUsageStats.writeTo(out);
searchCcsMetrics.writeTo(out);
esqlCcsMetrics.writeTo(out);
if (out.getTransportVersion().supports(PROJECT_ROUTING_USAGE_STATS)) {
projectRoutingUsageSnapshot.writeTo(out);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As per above, maybe optional object here would be better, for non-CPS cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

See this comment: #155997 (comment)

}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xcontent.ToXContentFragment;
import org.elasticsearch.xcontent.XContentBuilder;

Expand All @@ -38,6 +39,9 @@ public class ClusterStatsResponse extends BaseNodesResponse<ClusterStatsNodeResp
final RepositoryUsageStats repositoryUsageStats;
final CCSTelemetrySnapshot ccsMetrics;
final CCSTelemetrySnapshot esqlMetrics;
final ProjectRoutingUsageSnapshot projectRoutingUsageSnapshot;
@Nullable
final TagsConfigSnapshot tagsConfig;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

As noted above, here it's a bit inconsistent - one is nullable, the other is not. Is there a reason why? The seem to be both non-existant in non-CPS context, or am I missing something?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree this is a little confusing. It is true that ProjectRoutingUsage and TagsConfig will only be present in serverless and both absent (in the _cluster/stats output) for stateful. The reason for the different handling here (one is null, the other is never null but can be full of zeros) is that:

  • tagsConfig is nullable because it comes from an optional plugin registration that may not exist at all (no serverless plugin = null provider). Null here means "feature not wired up". It gets wired up only on the serverless side.
  • The projectRoutingUsageSnapshot comes from ProjectRoutingUsageHolder, which is a built-in holder that always exists in UsageService on the es-core side. It always has a valid snapshot to return.

Neither will "render" in _cluster/stats for stateful. For projectRoutingUsageSnapshot, it will not be included in the _cluster/stats output unlesstotalQueries > 0 . (see: https://github.com/elastic/elasticsearch/pull/155997/changes#diff-e4a6036f7e574278a739adbb118f2ab0031e4c1abbe92bac125bb732c8011993R187)

final long timestamp;
final String clusterUUID;
private final Map<String, RemoteClusterStats> remoteClustersStats;
Expand All @@ -56,7 +60,8 @@ public ClusterStatsResponse(
VersionStats versionStats,
ClusterSnapshotStats clusterSnapshotStats,
Map<String, RemoteClusterStats> remoteClustersStats,
boolean skipMRT
boolean skipMRT,
@Nullable TagsConfigSnapshot tagsConfig
) {
super(clusterName, nodes, failures);
this.clusterUUID = clusterUUID;
Expand All @@ -65,6 +70,7 @@ public ClusterStatsResponse(
indicesStats = new ClusterStatsIndices(nodes, mappingStats, analysisStats, versionStats);
ccsMetrics = new CCSTelemetrySnapshot(skipMRT == false);
esqlMetrics = new CCSTelemetrySnapshot(false);
projectRoutingUsageSnapshot = new ProjectRoutingUsageSnapshot();
ClusterHealthStatus status = null;
for (ClusterStatsNodeResponse response : nodes) {
// only the master node populates the status
Expand All @@ -76,9 +82,11 @@ public ClusterStatsResponse(
nodes.forEach(node -> {
ccsMetrics.add(node.getSearchCcsMetrics());
esqlMetrics.add(node.getEsqlCcsMetrics());
projectRoutingUsageSnapshot.add(node.getProjectRoutingUsageSnapshot());
});
this.status = status;
this.clusterSnapshotStats = clusterSnapshotStats;
this.tagsConfig = tagsConfig;

this.repositoryUsageStats = nodes.stream()
.map(ClusterStatsNodeResponse::repositoryUsageStats)
Expand Down Expand Up @@ -169,6 +177,20 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws

builder.endObject();

if (tagsConfig != null) {
builder.startObject("tags");
tagsConfig.toXContent(builder, params);
builder.endObject();
}

long totalQueries = projectRoutingUsageSnapshot.getSearchQueriesTotal() + projectRoutingUsageSnapshot.getEsqlQueriesTotal();
if (totalQueries > 0) {
builder.startObject("project_routing");
builder.field("queries", totalQueries);
projectRoutingUsageSnapshot.toXContent(builder, params);
builder.endObject();
}

return builder;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action.admin.cluster.stats;

import org.elasticsearch.cluster.ClusterState;
import org.elasticsearch.core.Nullable;

/**
* Extension point for supplying the {@code tags} configuration snapshot (tag names, named routing expressions, etc.)
* to {@code GET _cluster/stats}. Registered via SPI ({@code META-INF/services/}) and loaded by
* {@code NodeConstruction} using {@code loadSingletonServiceProvider}.
*
* <p>Needed as an extension point for serverless code.
*/
@FunctionalInterface
public interface ClusterStatsTagsProvider {

/**
* Returns the current tags configuration snapshot for the request's project, or {@code null} if the configuration
* is unavailable (e.g. CPS is disabled, or the project cannot be resolved from the thread context).
*/
@Nullable
TagsConfigSnapshot getTagsConfig(ClusterState clusterState);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

package org.elasticsearch.action.admin.cluster.stats;

import org.elasticsearch.core.Nullable;
import org.elasticsearch.search.crossproject.ProjectRoutingRequestInfo;

import java.util.concurrent.atomic.LongAdder;

/**
* Accumulates per-node project-routing telemetry counters. Follows the same pattern as {@link CCSUsageTelemetry}.
* Thread-safe via {@link LongAdder}. Obtain a point-in-time snapshot with {@link #getSnapshot()}.
*
* <p>All counters are gated on {@code hasLinkedProjects}: they only increment while the project has at least one
* configured linked project. This ensures percentages can be computed from the data
* (e.g. {@code queries_project_routing / queries}).
*
* <p>Common per-endpoint counters are grouped in {@link RoutingCounters}. Adding a new endpoint (e.g. EQL, SQL)
* requires adding a new {@link RoutingCounters} instance and a corresponding {@code record*()} method.
* The ES|QL-specific {@code in_SET} counter ({@link #esqlWithSet}) is tracked separately.
*/
public class ProjectRoutingUsageHolder {

/**
* Groups the counters that are common across all tracked endpoints. Each endpoint ({@code _search},
* {@code _esql}, and any future additions) gets its own instance.
*/
private static class RoutingCounters {
final LongAdder total = new LongAdder();
final LongAdder withProjectRouting = new LongAdder();
final LongAdder withAliasOrigin = new LongAdder();
final LongAdder withAliasWildcard = new LongAdder();
final LongAdder withCustomTags = new LongAdder();
final LongAdder withNamedExpression = new LongAdder();
final LongAdder failures = new LongAdder();

/**
* Records a query. Always increments {@code total}. When {@code info} is non-null (the request
* carried a {@code project_routing} expression), also increments {@code withProjectRouting} and
* any applicable sub-counters.
*/
void record(@Nullable ProjectRoutingRequestInfo info) {
total.increment();
if (info == null) {
return;
}
withProjectRouting.increment();
if (info.usedAliasOrigin()) {
withAliasOrigin.increment();
}
if (info.usedAliasWildcard()) {
withAliasWildcard.increment();
}
if (info.usedNamedExpression()) {
withNamedExpression.increment();
}
if (info.usedCustomTags()) {
withCustomTags.increment();
}
}

/**
* Records a routing failure. Increments {@code total}, {@code withProjectRouting}, and
* {@code failures}. Called by Ticket 5 from {@code AuthorizationService.onAuthorizedResourceLoadFailure()}.
*/
void recordFailure() {
total.increment();
withProjectRouting.increment();
failures.increment();
}
}

// _search, _async_search, _msearch (per sub-request), _search/template, _msearch/template, _count, _cat/count
private final RoutingCounters search = new RoutingCounters();

// ES|QL endpoint
private final RoutingCounters esql = new RoutingCounters();
private final LongAdder esqlWithSet = new LongAdder(); // in_SET: routing came from SET clause, not request body

/**
* Records a {@code _search} request. {@code queries} is always incremented (subject to the
* {@code hasLinkedProjects} gate). The {@code queries_project_routing} counter and its sub-counters are only
* incremented when {@code info} is non-null, i.e. the request carried a {@code project_routing} expression.
*
* @param info routing metadata from the resolver; null when the request had no {@code project_routing} header,
* or until the resolver is upgraded to populate it (Ticket 2)
* @param hasLinkedProjects true when the project had at least one linked project at the time of the request;
* when false all counters are skipped
*/
public void recordSearch(@Nullable ProjectRoutingRequestInfo info, boolean hasLinkedProjects) {
if (hasLinkedProjects == false) {
return;
}
search.record(info);
}

/**
* Records an ES|QL request. {@code queries} is always incremented (subject to the
* {@code hasLinkedProjects} gate). The {@code queries_project_routing} counter and its sub-counters are only
* incremented when {@code info} is non-null, i.e. the request carried a {@code project_routing} expression.
*
* @param info routing metadata from the resolver; null when the request had no {@code project_routing} expression,
* or until the resolver is upgraded to populate it (Ticket 2)
* @param setClauseUsed true when the routing expression came from an in-query {@code SET project_routing = ...} clause
* @param hasLinkedProjects true when the project had at least one linked project at the time of the request;
* when false all counters are skipped
*/
public void recordEsql(@Nullable ProjectRoutingRequestInfo info, boolean setClauseUsed, boolean hasLinkedProjects) {
if (hasLinkedProjects == false) {
return;
}
esql.record(info);
if (setClauseUsed && info != null) {
esqlWithSet.increment();
Comment thread
smalyshev marked this conversation as resolved.
}
}

/**
* Records a routing failure for a {@code _search}-family request. Increments {@code queries},
* {@code queries_project_routing}, and {@code failures}. Called by Ticket 5 from
* {@code AuthorizationService.onAuthorizedResourceLoadFailure()}.
*
* @param hasLinkedProjects true when the project had at least one linked project; when false this is a no-op
*/
public void recordSearchProjectRoutingFailure(boolean hasLinkedProjects) {
if (hasLinkedProjects == false) {
return;
}
search.recordFailure();
}

/**
* Records a routing failure for an ES|QL request. Increments {@code queries},
* {@code queries_project_routing}, and {@code failures}. Called by Ticket 5 from
* {@code AuthorizationService.onAuthorizedResourceLoadFailure()}.
*
* @param hasLinkedProjects true when the project had at least one linked project; when false this is a no-op
*/
public void recordEsqlProjectRoutingFailure(boolean hasLinkedProjects) {
if (hasLinkedProjects == false) {
return;
}
esql.recordFailure();
}

/**
* Returns a point-in-time snapshot of the current counters.
*/
public ProjectRoutingUsageSnapshot getSnapshot() {
return new ProjectRoutingUsageSnapshot(
search.total.sum(),
search.withProjectRouting.sum(),
search.withAliasOrigin.sum(),
search.withAliasWildcard.sum(),
search.withCustomTags.sum(),
search.withNamedExpression.sum(),
search.failures.sum(),
esql.total.sum(),
esql.withProjectRouting.sum(),
esql.withAliasOrigin.sum(),
esql.withAliasWildcard.sum(),
esql.withCustomTags.sum(),
esql.withNamedExpression.sum(),
esqlWithSet.sum(),
esql.failures.sum()
);
}
}
Loading
Loading