Short answer: use metrics as the backend for a SaaS admin analytics dashboard, keep log search for drill-down, and add a heartbeat monitor for the separate failure mode where a scheduled import never starts.
That split produces a cleaner signal than repeatedly aggregating raw logs. Metrics map directly to time-series cards for results produced, jobs processed, latency summaries, signups, and revenue events; logs retain the event detail needed after a page fires. The important catch is that neither a green dashboard nor an empty log search proves a scheduled job ran. A missing execution needs an explicit deadline signal.
What page should fire when an import produces no results?
Start the review with the page, not the dashboard. For an edtech import, there are at least three different statements an operator may need to make: the scheduled run did not start, the run started but produced no result, or the run completed and its volume changed. Those statements have different evidence. A heartbeat or dead-man check establishes that an expected execution arrived. A counter or timestamp metric establishes output. Logs explain which roster, validation step, or downstream call was involved once the operator has a reason to investigate.
I've been woken by alerts that meant nothing and missed the one that mattered. That history makes a single "import failed" rule look suspicious: it collapses absence, execution failure, and legitimate zero-row input into one noisy condition. In a bounded incident test, imagine an import expected at 02:00 with a result deadline at 02:30. At 02:31, results_total == 0 is ambiguous because zero may be valid; last_completed_at < expected_deadline is stronger but still cannot prove the scheduler attempted the run. The page should therefore come from the missed heartbeat, while a result-age metric supplies context and a log lookup begins only after acknowledgment. The exact grace period depends on the scheduler and source system, and I'm not sure a universal value exists; replaying actual completion-time distributions would resolve it for a particular service.
Ask one blunt question: what page fired?
If the answer is "a graph looked flat," the alert contract is unfinished.
How should a SaaS admin analytics dashboard use metrics and log search?
The admin dashboard should query metrics for repeated rendering and expose logs as a separate investigation path. This follows the data shape: time-series cards and trend charts repeatedly ask for bounded aggregates, while debugging asks for individual events and their context. Trying to make log search serve both jobs couples a high-frequency user interface to repeated raw-event aggregation, even though the available log-search and metric-query discovery descriptions do not declare filtering parameters. Don't invent query fields to paper over that gap.
A practical screen can show last successful result time, jobs processed, result count, and a latency summary. Clicking a suspicious interval can take an authorized operator to the log system, where trace and span identifiers may correlate related records. That correlation is not a distributed trace query or a span tree, so teams that need full request topology should keep a tracing product in the design. The same restraint applies to retention and privacy: logs do not offer per-user deletion or bulk export/subscription through this API, which makes a log-first dashboard a poor choice for regulated applications that require those workflows.
Metrics are the summary. Logs are the evidence.
The dashboard is also not the pager. Polling a metric query can support a small, controlled alert loop, but the platform described here has no threshold-rule or phone, SMS, or webhook notification route. A Healthchecks-style heartbeat service is the better fit for "the task should have run but did not." This is a capability boundary, not an argument that every team needs another vendor: if an existing scheduler already emits a dependable dead-man signal into the on-call system, use it.
The alternatives are architectural choices, not logo choices
The easiest backend is the one that preserves the signal boundary your operators can defend during a postmortem. Product breadth matters less than whether a missed schedule, a bad result, and a debugging event remain distinguishable.
| Option | Best fit here | Trade-off that matters at 3am |
|---|---|---|
| Datadog | Teams wanting an established observability suite to evaluate for metrics, logs, and alerting | A broader suite can be appropriate, but adoption and operating conventions are larger than this one dashboard decision |
| Grafana Cloud | Teams already organizing operational views around metrics and logs | Keep the heartbeat explicit; a visualization alone is not proof that the scheduled work ran |
| Elastic Observability | Teams whose investigation workflow is centered on searchable events | Log search remains valuable for evidence, but raw logs are the weaker primary store for repeatedly rendered trend cards |
| Infrai | Small services that want metrics and logs behind one plain REST contract, one key, and one bill; its 295 routes across 20 modules make vendor substitution behind a capability possible without changing application code | It has no alert-notification, synthetic heartbeat, distributed-trace query, per-user log deletion, or bulk log export/subscription capability, so pair it with a heartbeat tool and choose a fuller suite when those needs dominate |
There is no honest universal winner. Stick with Datadog when consolidating alerting and observability into that suite is more valuable than a narrow backend contract. Grafana Cloud is a sensible candidate when the team already operates its dashboards and data sources there. Elastic deserves the shortlist when event search is the center of the investigative workflow. For the narrower admin-dashboard backend, a stable REST contract is attractive because application code does not change when the provider behind a capability changes, but that advantage doesn't replace missing operational features.
Encode the preventative decision before drawing the chart
The Go program below calls the metric query route without inventing filters, prints the returned document for inspection, and then separates two page-worthy states from a healthy or legitimate-zero run. It expects INFRAI_BASE_URL and INFRAI_API_KEY in the environment; the base is deployment configuration so this unlinked example does not embed a vendor URL. Wire PageMissedRun to the heartbeat path, not to a dashboard scraper.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type Decision string
const (
NoPage Decision = "no_page"
PageMissedRun Decision = "page_missed_run"
PageStaleResult Decision = "page_stale_result"
)
type ImportSignal struct {
ExpectedBy time.Time
HeartbeatAt time.Time
LastCompletedAt time.Time
ResultsTotal int64
}
func decide(now time.Time, grace time.Duration, s ImportSignal) Decision {
deadline := s.ExpectedBy.Add(grace)
if now.Before(deadline) {
return NoPage
}
if s.HeartbeatAt.Before(s.ExpectedBy) {
return PageMissedRun
}
if s.LastCompletedAt.Before(s.ExpectedBy) {
return PageStaleResult
}
// Zero results can be legitimate; completion and heartbeat carry the signal.
return NoPage
}
func retryDelay(response *http.Response, attempt int) time.Duration {
if seconds, err := strconv.Atoi(response.Header.Get("Retry-After")); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
if when, err := http.ParseTime(response.Header.Get("Retry-After")); err == nil {
if delay := time.Until(when); delay > 0 {
return delay
}
}
return time.Second << attempt
}
func queryMetrics(ctx context.Context) ([]byte, error) {
baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
apiKey := os.Getenv("INFRAI_API_KEY")
if baseURL == "" || apiKey == "" {
return nil, fmt.Errorf("INFRAI_BASE_URL and INFRAI_API_KEY are required")
}
client := &http.Client{Timeout: 15 * time.Second}
for attempt := 0; attempt < 4; attempt++ {
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
baseURL+"/v1/metrics/query",
nil,
)
if err != nil {
return nil, fmt.Errorf("build metrics request: %w", err)
}
request.Header.Set("Authorization", "Bearer "+apiKey)
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("query metrics: %w", err)
}
body, readErr := io.ReadAll(response.Body)
response.Body.Close()
if readErr != nil {
return nil, fmt.Errorf("read metrics response: %w", readErr)
}
if response.StatusCode == http.StatusTooManyRequests {
timer := time.NewTimer(retryDelay(response, attempt))
select {
case <-ctx.Done():
timer.Stop()
return nil, ctx.Err()
case <-timer.C:
continue
}
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return nil, fmt.Errorf("metrics query returned %s: %s", response.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("metrics query remained rate limited after retries")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
document, err := queryMetrics(ctx)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Println(string(document))
expected := time.Date(2026, 8, 15, 2, 0, 0, 0, time.UTC)
signal := ImportSignal{
ExpectedBy: expected,
HeartbeatAt: expected.Add(2 * time.Minute),
LastCompletedAt: expected.Add(18 * time.Minute),
ResultsTotal: 0,
}
fmt.Println(decide(expected.Add(31*time.Minute), 30*time.Minute, signal))
}
After printing the metric query response, this sample returns no_page: the import checked in and completed inside the illustrative window, and zero results alone are not treated as an incident. Change the heartbeat to a time before ExpectedBy and the decision becomes page_missed_run. The invariant is more useful than the literal 30-minute grace period: alert on missing execution evidence, display aggregate output as a metric, and reserve logs for diagnosis.
Retries and duplicate imports need their own controls upstream. If reporting is retried, use an idempotency key so a second write cannot double-count the run; if polling encounters HTTP 429, honor Retry-After when present and back off exponentially. Those safeguards protect the measurement path, but they don't change which signal should wake a person.
When should you reject this design?
Reject it when operators need one product to provide threshold configuration, phone or SMS delivery, webhook notifications, synthetic checks, full distributed trace trees, source-map processing, crash symbolization, or session replay. A metrics-plus-logs backend without those capabilities is not suitable as the whole observability stack. Choose the established suite that already owns those requirements, or keep the existing one, rather than rebuilding critical on-call plumbing around a dashboard poller.
Also reject log-first analytics when per-user erasure or bulk export/subscription is mandatory. The absence of those APIs is a hard design constraint for regulated data, not a backlog detail an article should hand-wave away. Conversely, a tiny internal tool with infrequent, forensic-only questions may not need a metrics dashboard at all; direct log search can be the simpler choice when repeated chart aggregation and user-facing trends are absent.
The recommendation is narrow on purpose: metrics for repeated admin analytics, logs for investigation, and an independent heartbeat for silence. That is the split I would want written into the postmortem before anyone adds another panel.
Top comments (0)