Skip to content
Merged
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.
* `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|
|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
73 changes: 73 additions & 0 deletions server/src/main/java/org/apache/druid/server/BuildInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.druid.server;

import org.apache.druid.java.util.common.logger.Logger;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.jar.Manifest;

/**
* Utility class for reading build metadata from the JAR manifest.
*/
public class BuildInfo
{
private static final Logger log = new Logger(BuildInfo.class);

private BuildInfo()
{
// Utility class; do not instantiate.
}

/**
* Reads the {@code Build-Revision} attribute from the {@code META-INF/MANIFEST.MF} of the JAR
* that contains this class. Returns an empty string when running outside a packaged JAR
* (e.g., during {@code mvn test}).
*/
public static String getBuildRevision()
{
try {
URL classUrl = BuildInfo.class.getResource(BuildInfo.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 readRevisionFromManifest(is);
}
}
}
catch (IOException e) {
log.warn(e, "Failed to read Build-Revision from JAR manifest");
}
return "";
}

/**
* Reads the {@code Build-Revision} attribute from a manifest {@link InputStream}.
* Returns an empty string if the attribute is absent.
*/
static String readRevisionFromManifest(InputStream is) throws IOException
{
String revision = new Manifest(is).getMainAttributes().getValue("Build-Revision");
return revision != null ? revision : "";
}
}
9 changes: 9 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 @@ -92,6 +92,10 @@ public class DruidNode
UNKNOWN_VERSION
);

@JsonProperty
@NotNull
private final String buildRevision = BuildInfo.getBuildRevision();

@JsonProperty
private Map<String, String> labels;

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

public String getBuildRevision()
{
return buildRevision;
}

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 @@ -43,6 +43,7 @@
import org.apache.druid.java.util.emitter.core.Emitter;
import org.apache.druid.java.util.emitter.service.ServiceEmitter;
import org.apache.druid.java.util.metrics.TaskHolder;
import org.apache.druid.server.BuildInfo;
import org.apache.druid.server.DruidNode;

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

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

/**
* Returns the {@code Build-Revision} for the current build, delegating to {@link BuildInfo}.
* Overridable for testing.
*/
protected String getBuildRevision()
{
return BuildInfo.getBuildRevision();
}
}
54 changes: 54 additions & 0 deletions server/src/test/java/org/apache/druid/server/BuildInfoTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF 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.apache.druid.server;

import org.junit.Assert;
import org.junit.Test;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;

public class BuildInfoTest
{
@Test
public void testGetBuildRevisionReturnsEmptyStringOutsideJar()
{
// During mvn test the class loads from the filesystem, not a JAR, so this must return "".
Assert.assertEquals("", BuildInfo.getBuildRevision());
}

@Test
public void testReadRevisionFromManifestWithRevisionPresent() throws IOException
{
String manifest = "Manifest-Version: 1.0\nBuild-Revision: abc123\n\n";
InputStream is = new ByteArrayInputStream(manifest.getBytes(StandardCharsets.UTF_8));
Assert.assertEquals("abc123", BuildInfo.readRevisionFromManifest(is));
}

@Test
public void testReadRevisionFromManifestWithRevisionAbsent() throws IOException
{
String manifest = "Manifest-Version: 1.0\n\n";
InputStream is = new ByteArrayInputStream(manifest.getBytes(StandardCharsets.UTF_8));
Assert.assertEquals("", BuildInfo.readRevisionFromManifest(is));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,74 @@ 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 "" (e.g. running outside a packaged JAR), buildRevision dimension is an empty string.
EmitterModule emitterModule = new EmitterModule()
{
@Override
protected String getBuildRevision()
{
return "";
}
};
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