Skip to content
Open
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
2 changes: 2 additions & 0 deletions docs/operations/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ All Druid metrics share a common set of fields:
* `metric`: the name of the metric
* `service`: the service name that emitted the metric
* `host`: the host name that emitted the metric
* `version`: the Druid version of the service that emitted the metric
* `buildRevision`: the git commit of the build that produced the service binary. Useful for verifying that all nodes in a cluster are running the intended revision during rolling deployments. Empty string when running outside a packaged binary (e.g., during `mvn test`).
Copy link
Contributor

Choose a reason for hiding this comment

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

These are user-facing docs, so I think we can exclude the note about mvn test that's already captured in the javadocs for devs:

Suggested change
* `buildRevision`: the git commit of the build that produced the service binary. Useful for verifying that all nodes in a cluster are running the intended revision during rolling deployments. Empty string when running outside a packaged binary (e.g., during `mvn test`).
* `buildRevision`: the git commit of the build that produced the service binary. Useful for verifying that all nodes in a cluster are running the intended revision during rolling deployments.

* `value`: some numeric value associated with the metric

Metrics may have additional dimensions beyond those listed above.
Expand Down
1 change: 1 addition & 0 deletions docs/querying/sql-metadata-tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ Servers table lists all discovered servers in the cluster.
|is_leader|BIGINT|1 if the server is currently the 'leader' (for services which have the concept of leadership), otherwise 0 if the server is not the leader, or null if the server type does not have the concept of leadership|
|start_time|STRING|Timestamp in ISO8601 format when the server was announced in the cluster|
|version|VARCHAR|Druid version running on the server|
|build_revision|VARCHAR|The git commit of the build that produced the server binary. Empty string when running outside a packaged binary (e.g., during `mvn test`)|
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
|build_revision|VARCHAR|The git commit of the build that produced the server binary. Empty string when running outside a packaged binary (e.g., during `mvn test`)|
|build_revision|VARCHAR|The git commit of the build that produced the server binary.|

Copy link
Contributor

Choose a reason for hiding this comment

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

Optionally we could also wire this to the console's services view by default in web-console/src/views/services-view/services-view.tsx and web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap: https://github.com/apache/druid/pull/18542/changes#diff-07dc2b09eeb1fc84b83d5ec80b95e4cda9fb235200e26b37271cc73d8d62b883

|labels|VARCHAR|Labels for the server configured using the property [`druid.labels`](../configuration/index.md)|
|available_processors|BIGINT|Total number of CPU processors available to the server|
|total_memory|BIGINT|Total memory in bytes available to the server|
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1885,7 +1885,7 @@
</execution>
</executions>
<configuration>
<dotGitDirectory>${project.basedir}/.git</dotGitDirectory>
<dotGitDirectory>${session.executionRootDirectory}/.git</dotGitDirectory>
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this change needed? Or will it still work without this multi-module setting?

<dateFormatTimeZone>Etc/UTC</dateFormatTimeZone>
<skipPoms>false</skipPoms>
<format>json</format>
Expand Down
36 changes: 36 additions & 0 deletions server/src/main/java/org/apache/druid/server/DruidNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,21 @@
import org.apache.druid.common.utils.SocketUtil;
import org.apache.druid.java.util.common.IAE;
import org.apache.druid.java.util.common.ISE;
import org.apache.druid.java.util.common.StringUtils;

import javax.annotation.Nullable;
import javax.validation.constraints.Max;
import javax.validation.constraints.NotNull;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.Map;
import java.util.Objects;
import java.util.jar.Manifest;

/**
*
Expand Down Expand Up @@ -92,6 +97,10 @@ public class DruidNode
UNKNOWN_VERSION
);

@JsonProperty
@NotNull
private final String buildRevision = StringUtils.nullToEmptyNonDruidDataString(readBuildRevisionFromManifest());

@JsonProperty
private Map<String, String> labels;

Expand Down Expand Up @@ -266,6 +275,33 @@ public String getVersion()
return version;
}

public String getBuildRevision()
{
return buildRevision;
}

/**
* Reads the {@code Build-Revision} attribute from the MANIFEST.MF of the JAR containing this class.
* Returns null when running outside a packaged JAR (e.g., during {@code mvn test}).
*/
private static String readBuildRevisionFromManifest()
{
try {
URL classUrl = DruidNode.class.getResource(DruidNode.class.getSimpleName() + ".class");
if (classUrl != null && "jar".equals(classUrl.getProtocol())) {
String classPath = classUrl.toString();
String manifestPath = classPath.substring(0, classPath.lastIndexOf('!') + 1) + "/META-INF/MANIFEST.MF";
try (InputStream is = new URL(manifestPath).openStream()) {
return new Manifest(is).getMainAttributes().getValue("Build-Revision");
}
}
}
catch (IOException e) {
// Fall through and return null
}
return null;
}
Comment on lines +287 to +303
Copy link
Contributor

@abhishekrb19 abhishekrb19 Mar 24, 2026

Choose a reason for hiding this comment

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

We can create a class BuildInfo and move this utility to it so both EmitterModule and the DruidNode code here can share it. We can also add more build-related utilities to this class in the future.


public DruidNode withService(String service)
{
return new DruidNode(service, host, bindOnHost, plaintextPort, tlsPort, enablePlaintextPort, enableTlsPort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,15 @@
import org.apache.druid.java.util.metrics.TaskHolder;
import org.apache.druid.server.DruidNode;

import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.jar.Manifest;

/**
*
Expand Down Expand Up @@ -92,6 +96,9 @@ public void configure(Binder binder)
extraServiceDimensions
.addBinding("version")
.toInstance(StringUtils.nullToEmptyNonDruidDataString(version)); // Version is null during `mvn test`.
extraServiceDimensions
.addBinding("buildRevision")
.toInstance(StringUtils.nullToEmptyNonDruidDataString(getBuildRevision()));
}

@Provides
Expand Down Expand Up @@ -177,4 +184,26 @@ public Emitter get()
return emitter;
}
}

/**
* Reads the {@code Build-Revision} attribute from the MANIFEST.MF of the JAR that contains this class.
* Returns null when running outside a packaged JAR (e.g., during {@code mvn test}).
*/
protected String getBuildRevision()
{
try {
URL classUrl = EmitterModule.class.getResource(EmitterModule.class.getSimpleName() + ".class");
if (classUrl != null && "jar".equals(classUrl.getProtocol())) {
String classPath = classUrl.toString();
String manifestPath = classPath.substring(0, classPath.lastIndexOf('!') + 1) + "/META-INF/MANIFEST.MF";
try (InputStream is = new URL(manifestPath).openStream()) {
return new Manifest(is).getMainAttributes().getValue("Build-Revision");
}
}
}
catch (IOException e) {
// Fall through and return null
Copy link
Contributor

Choose a reason for hiding this comment

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

Log the exception so it doesn't mask a legitimate issue

}
return null;
Copy link
Contributor

Choose a reason for hiding this comment

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

Since both EmitterModule and DruidNode converts null to empty with nullToEmptyNonDruidDataString(), I think it'd be good to have this method directly return "" (for tests)

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,75 @@ public void configure(Binder binder)
)
);
}

@Test
public void testBuildRevisionDimensionEmitsKnownValue()
{
// EmitterModule with a known revision verifies that getBuildRevision() is wired into the buildRevision dimension.
EmitterModule emitterModule = new EmitterModule()
{
@Override
protected String getBuildRevision()
{
return "abc1234def567890";
}
};
Injector injector = makeInjectorForEmitterModule(emitterModule);
ServiceEmitter serviceEmitter = injector.getInstance(ServiceEmitter.class);
serviceEmitter.start();
serviceEmitter.emit(new ServiceMetricEvent.Builder().setMetric("test", 1));

StubServiceEmitter stubEmitter = (StubServiceEmitter) injector.getInstance(Emitter.class);
EventMap map = ((ServiceMetricEvent) stubEmitter.getEvents().get(0)).toMap();
Assert.assertEquals("abc1234def567890", map.get("buildRevision"));
}

@Test
public void testBuildRevisionDimensionFallsBackToEmptyStringWhenUnavailable()
{
// When getBuildRevision() returns null (e.g. manifest absent), buildRevision dimension is an empty string,
// consistent with how the "version" dimension behaves during `mvn test`.
EmitterModule emitterModule = new EmitterModule()
{
@Override
protected String getBuildRevision()
{
return null;
}
};
Injector injector = makeInjectorForEmitterModule(emitterModule);
ServiceEmitter serviceEmitter = injector.getInstance(ServiceEmitter.class);
serviceEmitter.start();
serviceEmitter.emit(new ServiceMetricEvent.Builder().setMetric("test", 1));

StubServiceEmitter stubEmitter = (StubServiceEmitter) injector.getInstance(Emitter.class);
EventMap map = ((ServiceMetricEvent) stubEmitter.getEvents().get(0)).toMap();
Assert.assertEquals("", map.get("buildRevision"));
}

private Injector makeInjectorForEmitterModule(EmitterModule emitterModule)
{
Properties props = new Properties();
props.setProperty("druid.emitter", "stub");
emitterModule.setProps(props);
return Guice.createInjector(
new JacksonModule(),
new LifecycleModule(),
binder -> {
JsonConfigProvider.bindInstance(
binder,
Key.get(DruidNode.class, Self.class),
new DruidNode("test-service", "localhost", false, 8080, null, true, false)
);
binder.bind(Validator.class).toInstance(Validation.buildDefaultValidatorFactory().getValidator());
binder.bindScope(LazySingleton.class, Scopes.SINGLETON);
binder.bind(Properties.class).toInstance(props);
binder.bind(TaskHolder.class).toInstance(new TestTaskHolder("test", "id1", "type1", "group1"));
binder.bind(LoadSpecHolder.class).to(DefaultLoadSpecHolder.class).in(LazySingleton.class);
},
ServerInjectorBuilder.registerNodeRoleModule(ImmutableSet.of()),
emitterModule,
new StubServiceEmitterModule()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@ public class SystemSchema extends AbstractSchema
.add("is_leader", ColumnType.LONG)
.add("start_time", ColumnType.STRING)
.add("version", ColumnType.STRING)
.add("build_revision", ColumnType.STRING)
.add("labels", ColumnType.STRING)
.add("available_processors", ColumnType.LONG)
.add("total_memory", ColumnType.LONG)
Expand Down Expand Up @@ -697,6 +698,7 @@ private Object[] buildRowForNonDataServer(DiscoveryDruidNode discoveryDruidNode)
null,
toStringOrNull(discoveryDruidNode.getStartTime()),
node.getVersion(),
node.getBuildRevision(),
node.getLabels() == null ? null : JacksonUtils.writeValueAsString(jsonMapper, node.getLabels()),
(long) discoveryDruidNode.getAvailableProcessors(),
discoveryDruidNode.getTotalMemory()
Expand Down Expand Up @@ -725,6 +727,7 @@ private Object[] buildRowForNonDataServerWithLeadership(
isLeader ? 1L : 0L,
toStringOrNull(discoveryDruidNode.getStartTime()),
node.getVersion(),
node.getBuildRevision(),
node.getLabels() == null ? null : JacksonUtils.writeValueAsString(jsonMapper, node.getLabels()),
(long) discoveryDruidNode.getAvailableProcessors(),
discoveryDruidNode.getTotalMemory()
Expand Down Expand Up @@ -765,6 +768,7 @@ private Object[] buildRowForDiscoverableDataServer(
null,
toStringOrNull(discoveryDruidNode.getStartTime()),
node.getVersion(),
node.getBuildRevision(),
node.getLabels() == null ? null : JacksonUtils.writeValueAsString(jsonMapper, node.getLabels()),
(long) discoveryDruidNode.getAvailableProcessors(),
discoveryDruidNode.getTotalMemory()
Expand Down
Loading
Loading