Lesson 16 / 19
Health and Metrics Endpoints
An application comes up with endpoints that exist without ever being written — the container itself adds them; a new metrics endpoint can be added with an annotation. The limiting measurement closes in a separate narrowing section — the default endpoints the container builds do not open every piece of information at all, which field gets given out depends on an explicit mark, and if that mark is placed on the wrong field, the container gives it out too, with no error at all.
Published
In this lesson
- Counts, the moment the container is built, how many endpoints exist without any application class ever being written, and compares this against the endpoints added by an annotation
- Shows that the total number of fields a component carries is not the same as the number of fields given out by an annotation
- Measures that a metrics mark placed on the wrong field is given out by the IoC container with no error at all
Contents
The previous lesson counted which components were built — the slice test built two, the full context test built four, and both were the result of a decision written in the source: the binding table. This lesson asks about something never built, never written at all: there are endpoints that ask from the outside whether an application is in working order, and most of these endpoints never appear in the source at all — no class, no method, no annotation. The concept of health and metrics endpoints and the general framework of observability were built in the Observability and Operations course and are not repeated here; this lesson measures only how much trace these endpoints leave in the source and which information gets given out. No real metric value, no real duration, and no real memory usage is written; what is counted is the endpoint count and field presence. The third part of the measurement sits in a separate narrowing section, because the existence of default endpoints does not mean every piece of information gets given out — that is a separate decision.
In a real application a health endpoint usually returns a single word — is it working or not —
and a metrics endpoint carries counters, rates, internal state. This lesson models both, but the
question it asks is not the difference between health and metrics; it is what both have in
common: the ability to exist without a call in the source. /health is one health endpoint,
every endpoint carrying the /metrics/… prefix is a metrics endpoint, but both follow the same
rule — the container itself either adds them automatically or reads them from a mark.
Endpoints: Ones Never Appearing in the Source, and Ones Coming from an Annotation
The modeled IoC container is born with two endpoints the moment it is built: /health and
/liveness. Neither of these two endpoints is the result of any application class, of any
annotation — they are written into the container’s constructor itself, an automatic pair
existing at every build. There is also a third kind of endpoint: a @MetricsEndpoint mark
placed on a component’s field, read by the container and registered as a new endpoint.
// MetricsEndpoint.java — annotation type marking a field as a metrics endpoint to be given out (helper source, no main)
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD)
@interface MetricsEndpoint { String name(); }
// Container.java — small IoC container that brings two default endpoints into existence as soon as it is built, and can add a new endpoint from an annotation (helper source, no main)
import java.lang.reflect.*;
import java.util.*;
import java.util.function.*;
class Container {
final Map<String, Supplier<String>> endpoints = new LinkedHashMap<>();
final int defaultEndpointCount;
Container() {
// default endpoints: no application class ever calls these at all, the container itself adds them
endpoints.put("/health", () -> "running");
endpoints.put("/liveness", () -> "yes");
defaultEndpointCount = endpoints.size();
}
void register(Object component) {
for (Field f : component.getClass().getDeclaredFields()) {
MetricsEndpoint m = f.getAnnotation(MetricsEndpoint.class);
if (m == null) continue;
f.setAccessible(true);
endpoints.put("/metrics/" + m.name(), () -> {
try { return String.valueOf(f.get(component)); }
catch (Exception e) { return "?"; }
});
}
}
String call(String path) {
Supplier<String> s = endpoints.get(path);
return s == null ? "none" : s.get();
}
static void show(String label, Object value) {
System.out.printf("%-46s: %s%n", label, value);
}
}
// Application.java — the request counter is marked to be given out, the remaining two fields are never marked at all (helper source, no main)
class Application {
@MetricsEndpoint(name = "request-count") int requestCounter = 42;
String internalKey = "internal-config";
String version = "3";
}
// DefaultEndpoints.java — the number of endpoints existing as soon as the container is built, and the total after a component is registered
public class DefaultEndpoints {
public static void main(String[] args) {
Container container = new Container();
Container.show("number of endpoints existing as soon as the container is built", container.defaultEndpointCount);
Container.show("number of application classes calling these endpoints in the source", 0);
container.register(new Application());
Container.show("total endpoint count after Application is registered", container.endpoints.size());
Container.show("/health", container.call("/health"));
Container.show("/liveness", container.call("/liveness"));
Container.show("/metrics/request-count", container.call("/metrics/request-count"));
}
}
number of endpoints existing as soon as the container is built: 2 number of application classes calling these endpoints in the source: 0 total endpoint count after Application is registered: 3 /health : running /liveness : yes /metrics/request-count : 42
ST17. The moment the container is built, endpoints exist that no application class ever
wrote at all; these are born from the default — the source-level counterpart of /health and
/liveness is two lines in Container’s constructor, and no application code calls these lines
at all. ST18. A new metrics endpoint is added by an annotation; the @MetricsEndpoint mark
above the requestCounter field gives rise to the /metrics/request-count endpoint, but the
call that reads this endpoint does not exist anywhere inside the Application class at all —
the party reading and registering it is the Container itself. Two of the total three
endpoints come into existence without ever appearing in the source, one comes into existence
with a mark; someone only reading the source sees a single line in Application.java, but the
surface the application gives out to the world consists of three endpoints.
The count of default endpoints is fixed and readable in Container’s constructor; learning how
many there are needs no scan of any application class at all — the defaultEndpointCount field
carries this number the moment it is built. The situation differs for annotation-derived
endpoints: their count depends on how many times and with which classes register is called,
and this count is only settled once every component of the application has been registered.
This difference between the two kinds — one fixed, one growing at build time — will reappear
later in the lesson: the distributed configuration lesson will also have some values fixed at
build time while others can change after construction.
What Gets Given Out Is a Separate Decision
The Application class carries three fields, but only one appears under /metrics. The
remaining two fields — internalKey, version — sit inside the class; no endpoint mentions
them at all, because they carry no mark. This is the lesson’s most important distinction: the
amount of information a component carries and the amount of information that component gives
out are not the same thing.
// FieldCount.java — the total number of fields Application carries is compared against the number of marked fields
import java.lang.reflect.*;
public class FieldCount {
public static void main(String[] args) {
Container container = new Container();
Application application = new Application();
container.register(application);
int totalFields = Application.class.getDeclaredFields().length;
long markedFields = 0;
for (Field f : Application.class.getDeclaredFields()) {
if (f.isAnnotationPresent(MetricsEndpoint.class)) markedFields++;
}
Container.show("total number of fields carried by the Application class", totalFields);
Container.show("number of fields marked with @MetricsEndpoint", markedFields);
Container.show("/metrics/internal-key (never marked)", container.call("/metrics/internal-key"));
Container.show("/metrics/version (never marked)", container.call("/metrics/version"));
}
}
total number of fields carried by the Application class: 3 number of fields marked with @MetricsEndpoint : 1 /metrics/internal-key (never marked) : none /metrics/version (never marked) : none
Only one of the three fields becomes visible; for the remaining two, the call method returns
none, because Container‘s endpoints map has no such key registered at all. ST19. The
number of fields a component carries and the number of fields given out are not the same;
only marked fields get given out, and every unmarked field stays a field the container never
knows about at all. ST20. This is the first half of the lesson’s limiting measurement: the
existence of default endpoints does not mean every piece of information gets opened up.
/health and /liveness exist, but neither touches Application’s internal state at all;
what comes out of that internal state is determined solely by marking, and nothing comes out
without a mark. Container’s register method sees every field with
Application.class.getDeclaredFields() — internalKey and version also pass through this
scan — but every unmarked field is skipped in the loop with continue. Being seen and being
given out are therefore not the same thing: the container sees every field, and registers only
the marked one.
The Cost of Extension: A Field Marked by Mistake
The marking mechanism works in two directions: marking a field with @MetricsEndpoint makes it
visible, but placing the mark on the correct field is a separate responsibility. Container’s
register method processes every marked field the same way — it carries no distinction at all
between which field is “safe” and which is “sensitive.”
// SensitiveComponent.java — both fields are marked; one is the intended counter, the other is internal information marked by mistake (helper source, no main)
class SensitiveComponent {
@MetricsEndpoint(name = "config-key") String internalKey = "internal-config-value";
@MetricsEndpoint(name = "active-sessions") int activeSessionCount = 7;
}
// WrongMark.java — the container processes both marks the same way, never distinguishing which one was placed by mistake
public class WrongMark {
public static void main(String[] args) {
Container container = new Container();
container.register(new SensitiveComponent());
Container.show("/metrics/active-sessions (the intended metric)", container.call("/metrics/active-sessions"));
Container.show("/metrics/config-key (marked by mistake)", container.call("/metrics/config-key"));
Container.show("did the container reject this mark", "no, no error was raised at all");
}
}
/metrics/active-sessions (the intended metric): 7 /metrics/config-key (marked by mistake) : internal-config-value did the container reject this mark : no, no error was raised at all
activeSessionCount measures exactly what was intended — how many sessions are open.
internalKey was given out by mistake with the same mark, and Container does not
distinguish between the two cases at all: both carry @MetricsEndpoint, both pass through the
same path and get registered the same way. The register method itself sees no difference
between these two fields at all, because it has no information available to observe any
difference in the first place — all it holds is a field name, an annotation, and a value;
whether the field is “sensitive” or “harmless” has no classification present in the reflection
itself. The only party that can make this classification is whoever places the
@MetricsEndpoint mark, and once that decision is made wrong even once, the container itself
never corrects it. ST21. A metrics mark placed on the wrong field is given out by the IoC
container with no error at all; this is the second half of the lesson’s limiting measurement.
The first half showed what does not get given out — an unmarked field never comes out at
all; the second half shows what can get given out by mistake — everything marked comes out,
whether it was marked correctly or not. The visibility built by health and metrics endpoints is
therefore not a one-way safety net; which information gets given out depends as much on the sum
of the marks in the source as on whether those marks were placed correctly.
The lesson’s four measurements gather where each piece of information comes from and where it ends up into a single table:
| Question | Answer | Its counterpart in the source |
|---|---|---|
| How many endpoints existed without any code ever being written | 2 (/health, /liveness) |
none — the container’s constructor |
| How many endpoints were added by an annotation | 1 (/metrics/request-count) |
the @MetricsEndpoint mark |
| How many fields are carried, how many are given out | 3 fields, 1 given out | the presence of the mark |
| Is a mistakenly marked field rejected | no | the correctness of the mark is never checked |
The last row of the table belongs to a different class than the first three: the first three answer “what exists” with a number, the last row answers “how reliable is this number” — and the answer is negative. An endpoint’s existence does not mean it exists for the correct purpose.
Summary
- The moment the container is built, endpoints exist that no application class ever wrote; these are born from the default.
- A new metrics endpoint is added by an annotation; no line in the source calls this endpoint.
- The total number of fields a component carries and the number of fields given out are not the same; only marked fields get given out.
- The existence of default endpoints does not mean every piece of information gets given out; which information is opened up is a separate, explicit marking decision.
- A metrics mark placed on the wrong field is given out by the IoC container with no error at all; the marking mechanism does not distinguish “correct” marking from “incorrect.”
What the four measurements have in common is that all three share the same source: the
container’s own decision. The two default endpoints sit in the container’s constructor; the
acceptance of annotation-based endpoints and the failure to notice a wrong mark both sit in what
the register method does, or does not do. The application code gives neither of these three
decisions; it only places the mark, or does not, and the rest is the container’s rule.
Next Step
This lesson measured how a component’s internal state leaks to the outside. The next lesson asks the reverse: how does a value from the outside get in, and once it gets in, which component sees it when that value changes. If several parts of an application look at the same configuration value, whether all of them find out at the same moment the value changes is not obvious at all — the next lesson measures this by running it.
To keep your progress and take notes, Log in
My notes
Log in to take notes.