The views
What each page in the console shows, where the numbers come from, and which pages can change something.
Most pages read one subscription: the region and subscription selected in the header. Changing the selection reloads those pages. Two pages are exceptions: developer resources come from a committed catalog rather than a subscription, and Logs reads Datadog, which indexes every cluster in one place.
Overview
Counts from the generated catalog, plus which capabilities the selected subscription has. This page answers "what should be running here"; the rest answer "what is". The caller's role is in the header subscription popper, under Signed in: Admin, Engineer, or Viewer.
Services
The catalog tree: each service, MCP gateway, and app, the processes it starts, and the domains a
service owns. It comes from catalog.generated.yaml, which is derived from the Azure pipelines, so
a process that deploys is a process that appears. Pipelines under services/ are tagged
Service; pipelines under mcp/ are tagged MCP; pipelines under frontend/ are tagged
App. A domain with no subscribers still appears, with an empty list. A service, process, or
domain that arrives without a name is left out of the tree rather than crashing the page.
A process links to Containers filtered to that Helm release (the name on the listener, such as
template-domain1listener). A domain links to its outbox row.
Containers
Pods in the cluster, grouped by the service that owns them, with a namespace filter and a name search. Each pod card shows current CPU and memory utilization when the cluster's metrics API has reported that pod: a percent of the request (or of the limit if there is no request), and a hover for the raw amounts. The detail bar repeats the same numbers as used / requested. A card with no numbers means metrics-server did not answer for that pod, not that the pod is idle. Selecting a pod shows its containers, its recent cluster events, and two panes:
- Logs stream over server-sent events. The requested tail arrives first and the stream stays open, so there is no snapshot to race with the follow. Auto-scroll stops when you scroll up.
- Terminal opens a shell in the container over a WebSocket. This needs the admin role.
- YAML shows the pod manifest as the cluster has it.
Events sit next to the pod deliberately: a pod stuck in Pending has an empty log and an event
that explains why.
What the page is showing is kept in the address bar next to region and subscription, so a link
lands the next person on the same pod: namespace, nodePool, podSearch, pod, container,
tab, and the process, release and domain filters a link from the service tree sets. A
parameter is left out while it holds its default, and one naming a pod or container the cluster no
longer has falls back to the first pod once the list has loaded rather than showing an error.
Reading the log pane
Four toolbar toggles change how the stream reads, none of which touch what is downloaded — the download is always the raw lines.
| Control | Does |
|---|---|
| Search | Opens a find bar over the log output. Ctrl/Cmd+F while the pointer is in the pane does the same |
| Wrap | Wraps long lines instead of scrolling them sideways |
| Pretty-print | Renders full JSON lines the way the local console handler does |
| Scroll to bottom | Returns to the tail and resumes auto-scroll |
Search highlights every match and steps between them with Enter and Shift+Enter, or the
chevrons; Esc closes it. It does not filter the log, and it matches what is on screen — with
pretty-print on, that is the formatted text. Navigating to a match pauses auto-scroll so an
arriving line cannot pull the match out from under you.
Pretty-print only touches lines that are a complete JSON object, so a pod that mixes structured
records with plain output stays readable. Those lines render in the format from
ADR 0017: a header of
timestamp, level, serviceDomain, and message, then one indented attribute per line.
Logs
Retained logs across every service, from Datadog. Containers shows what one pod is saying now; this page answers what anything said earlier — including a pod that no longer exists.
The window picker sits top-right and defaults to the last fifteen minutes. The search box takes Datadog log syntax, the facet rail on the left narrows the result set, and a row opens a drawer with the full message and its attributes. Open in Datadog hands the same query and window to the Datadog Log Explorer. The chevron in the rule beside the rail collapses it, giving the width to the message column. On a narrow screen the rail moves into a drawer opened from the Facets button above the table.
The rail is a list of what can be filtered on, not a breakdown of what is on screen, so it does not
change with the search. It is fetched when the view opens and then holds still, except while it is
still arriving: devops-service reads it a field at a time, so a group can appear a moment after
the ones above it. A group is only drawn once it has values to click — a field still being read and
a field with nothing to offer look the same from here, and a row that says it is loading is
indistinguishable from one that is stuck. Ticking a value reloads the table
underneath it and leaves the rail alone, which is what lets a second value be added to the first
without the group it came from rearranging under the cursor. It carries no counts for the same
reason: the values are read by devops-service at startup, over the preceding fortnight, rather
than counted per search.
Values are checkboxes: several ticked in one group are alternatives (status:(error OR warning)),
while different groups narrow each other. Hovering a value offers Only to jump straight to it,
and a group with anything ticked offers Clear.
A group shows its five commonest values and Show more reveals the rest, which for a field like host is the whole fortnight's worth. A ticked value stays visible even when it sits below the cut, so a filter is never applied by something that cannot be seen or cleared. The chevron beside the group name hides it altogether.
Which fields the rail offers is configuration, not code: services/devops/registry/logfacets.yaml
lists them, because Datadog publishes no API that returns an organization's configured facets. Only
fields with something to click appear; one that is not a facet in Datadog, or that nothing carried
when the list was read, is left out rather than shown as an empty group. devops-service logs which
and why. Log level is ordered by severity rather than by volume, so errors do not sit below info.
That ordering is not a filter: a status the file does not name, because Datadog spelled it
differently or a service invented it, still appears in the rail after the ones that are named.
The env: chip beside the search box is fixed, not a default. devops-service adds the
environment filter to every query before it leaves the process, so a dev console cannot read
production logs. Where the credentials are not configured, the page says so instead of failing.
Deployments
Helm releases and the revision each one is running. A release the catalog does not describe is marked rather than hidden — it is either a new pipeline or a stale deploy, and both are worth seeing. The history icon next to a release name lists every revision without offering a rollback.
Rollback replaces the running image and nothing else. It does not undo a database migration: if the deploy being backed out of migrated the schema, the older image starts against the newer schema. The dialog says so, and asks for the release name to be typed before it will proceed.
Outbox
Per domain, how many rows still have a null published_at, and how long the oldest has waited.
The age matters more than the count: a thousand rows that are all a second old is a busy minute,
while three rows twenty minutes old is a publisher that has stopped. A domain turns stale once its
oldest pending event passes the threshold the service reports.
Clicking a domain opens the newest twenty rows from that outbox — published or not. Expanding a row shows its payload. Draining belongs to the publisher.
Service Bus
Queues, topics, and subscriptions with their active and dead-letter counts, and a peek that reads message bodies without consuming or locking them — including from the dead-letter sub-queue.
A topic holds no messages of its own and a subscription that auto-forwards is drained on arrival, so neither offers a peek.
Database
The SQL console, on a session Postgres itself holds read-only. A statement that would write is refused with a sentence rather than a Postgres error about a read-only transaction. Results can be exported as CSV or JSON, and the row limit is reported when it cuts a result short.
Feature Flags
Feature flags for the selected subscription, read from the FeatureHub instance bound to it. The dropdown beside the title picks a portfolio, which is FeatureHub's grouping of applications that share service accounts. Three tabs sit under it.
FeatureHub has environments of its own, named things like development and production. They are not the region and subscription in the header: the header picks which FeatureHub instance you are looking at, and these are divisions inside it.
What the page is showing is kept in the address bar next to region and subscription, so a link
lands the next person on the same view: tab, portfolio, application, environments (the
visible columns, comma separated), featureSearch, and accountSearch. A parameter is left out
while it holds its default, and one naming something this subscription does not have falls back to
the default rather than showing an empty page.
Applications
A grid of every feature in the selected application, one row per feature and one column per environment, so a flag's value in development and in production can be read side by side. The Environments picker hides columns, which matters once an application has more than a screen's worth. Production sorts leftmost.
Each cell has a lock and a value. FeatureHub's lock is what stops a value changing, and unlocking is
a separate click from editing on purpose — it is the moment to notice which column you are in. A
boolean is a switch; the other types are text fields that save on blur or Enter rather than on
each keystroke, since every save is a write that polling SDKs then pick up.
A column whose header says read-only is one the proxy account may read but not change, so the controls are disabled rather than failing on save. Creating, editing details, and deleting features needs the engineer role; a new feature appears in every environment at once, unset and locked.
The clock icon on a row opens that feature's change history in a drawer: who set which value, when,
in which environment. FeatureHub records a version for every write. The table shows the most recent
twenty, newest first. History is a read, so anyone who can see the grid can open it. Changes made
from this console are recorded as the FeatureHub account devops-service logs in with, not as the
operator.
The pencil icon opens Edit feature. That is FeatureHub's feature details: the name shown in the console, an optional description, an optional documentation link, and whether the value is withheld from client-eval SDKs. The key and the value type cannot change after create.
Strategies
The branch icon in a cell opens the strategies for that feature in that environment. A strategy serves its own value to part of the audience: a name, the value it serves, an optional percentage, and any number of rules deciding who it applies to.
The order of the list is the order FeatureHub evaluates it in. It stops at the first strategy that matches, so moving one earlier with the arrow buttons changes what is served. Anything matching nothing gets the cell's own value, which is why that value is labelled default once a strategy exists — with strategies in play it is no longer what everyone sees, only what is left over.
A rule is a field, how to compare it, and the values to compare against. Two kinds are offered:
- A user key rule matches the
userkeyattribute, which is who the SDK says the request is for. - A custom rule matches any field name the application puts in its SDK context, compared as
text, a number, a boolean, a version, a date, or an IP address. The type decides how the
comparison is made, so a number compared as text sorts
10before9.
Several values in one rule are alternatives — any one of them matching is enough. Several rules on one strategy all have to match.
A percentage split needs a userKey or a sessionId in the SDK context. Without one, nothing
matches the strategy at all, and the flag silently falls back to the default. The split is also
approximate: it is a hash of the key, so 10% of forty users is not four of them.
Percentages across a value's strategies cannot add up to more than 100. The dialog checks that, plus
the two mistakes FeatureHub accepts and then ignores in silence: a strategy with no percentage and no
rules, and a rule with nothing to match against. Saving replaces the whole list, and a strategy
switched off keeps its rules but takes no part in the evaluation. Editing strategies needs the
engineer role, and the same CHANGE_VALUE permission the value itself does.
Create application adds an application to the selected portfolio. It asks for a name and a
description, both of which FeatureHub requires, and the new application arrives with no features
and two environments: the Production one FeatureHub creates for it, and a development one
devops-service adds alongside, so a flag has somewhere to be tried before production. Further
environments are added in FeatureHub itself. The same action is offered on the empty state, so a
portfolio with no applications can be filled from here. It needs the engineer role.
API Keys
Every service account paired with each environment it can reach, its permissions, and its two keys. The client-eval key downloads the whole ruleset and evaluates in the SDK; the server-eval key evaluates one user context per request.
Keys are masked until someone with the engineer role clicks Reveal, and that is not decoration:
the list genuinely does not contain them. Revealing one is a separate request that devops-service
logs with who asked. A leaked client-eval key reads every flag value in its environment — reissue it
from the Service Accounts tab.
Service Accounts
The non-human consumers of the portfolio's flags. Creating one asks for a name and a grid of
per-environment permissions; FeatureHub mints a key pair for each environment it can read, which
the API Keys tab then shows. Ticking anything adds READ with it, because no other permission means
anything without it and no key is issued without it.
Editing permissions replaces the account's access rather than adding to it, so an environment left unticked is revoked and its key stops working. Reissuing keys and deleting an account both take effect immediately for anything still polling, so both ask first. All three actions need the engineer role.
Resources
Cards for consoles operators already use — Datadog, Rootly, Snyk, Cloudflare, Azure DevOps, Azure
Portal — grouped as External. Each card is a name, a description, and a link that opens outside
the app. Logos are optional; a placeholder shows until a logo URL is set.
The list is frontend/devops-app/src/const/resources.yaml. Add a resource
under a group, or add a group (an Internal group for hosted tools is the intended next one),
and it appears on the page. This view does not call devops-service.