Every released version, newest first. Taken directly from the library’s
CHANGELOG.md, which is updated in the same
pull request as the change it describes.
Releases are published by CI from the tag. Pushing a v* tag now checks
that CHANGELOG.md has a stamped section matching it, then publishes the
GitHub release with that section as the notes. A tag pushed without stamping
used to produce a release with no notes and a docs site that silently showed
nothing new; nothing caught it. The stamping itself stays a reviewed pull
request — deciding what shipped is not a job for a script
Exporters page. Prometheus and OTel status, at
GET /monigo/api/v1/exporters and in the dashboard nav. Push and pull are
reported in their own terms rather than flattened into one pass/fail:
Prometheus scrapes MoniGo, so it shows when it was last scraped and carries
no failure count — a scrape that fails, fails at the collector and the
server never learns of it — while a push exporter shows last attempt, last
success, consecutive failures and the transport error verbatim
go get downloads 25 MB less.monigo.gif was 24.7 MB of a 25 MB module
zip — 96% of what every consumer fetched, on every build, was a README image
the compiler never looks at. It now lives on the assets branch, which is
never tagged and so is not part of any module version, and the README
references it by absolute URL. GitHub renders it identically.
TestDocumentationMediaDoesNotShipInTheModule fails if documentation media
is added back inside the module
new_static_mockups/ is deleted — 148 KB of design scaffolding from the
dashboard rebuild, referenced by no Go code and shipped to every consumer.
The dashboard it described is live; git history keeps it
Local agent tooling (.agents/, .claude/, skills-lock.json) is
gitignored. It is per-developer, and anything at the module root ships to
consumers
SetSamplingRate and WithSamplingRate now document the cost of a sampled
call: pprof.StopCPUProfile blocks on the runtime’s buffer flush for a
roughly constant ~200ms regardless of how long the function ran, measured at
201.8ms of overhead on a 1ms call. ExecutionTime is captured before the
stop, so the metric does not show it
13 compiled example binaries are no longer tracked — 502 MB, 95% of the
repository. They were macOS arm64, so they could not run on Linux, Windows or
an Intel Mac and were unusable to almost everyone who downloaded them. Now
ignored by explicit path: they take the name of their directory and have no
extension, so none of *.exe, *.dylib, /bin or *.test ever matched,
and no glob can separate them from an extensionless source file.
TestNoCompiledBinaryIsTracked checks the magic number of every tracked file
instead. Existing clones keep their history until it is rewritten separately
Service-health CPU was inflated by 100/TotalCores.getServiceCPUUsage
returns percent of one core — the top convention gopsutil follows — and
the health calculation divided it by the core count and then multiplied by
100, scaling a value that was already a percentage. The same measurement read
1.68% under SERVICE CPU LOAD and 17.66% in the health message on a
10-core host; the error grows as machines get smaller, 25x on four cores. An
idle process measured 360% on one core and 132% on four, both over a
95% limit — which, now that a breach is any single resource over its own
limit, was enough to report a healthy service as unhealthy. Both paths now
report percent of one core, and a test fails if the reading scales with core
count again
The health badge contradicted the sentence beside it, and flapped.Healthy came from the composite score crossing 50, while the message under
it came from a threshold comparison, and nothing kept the two in step: the
dashboard rendered Degraded next to “System usage is within limits” with
every number in the card inside its limit. Because the score drifts, the
verdict also oscillated — measured crossing the boundary twice in 45 seconds
on an idle machine while the bars underneath did not move. The verdict now
comes from the same threshold check that writes the message. The score is
untouched and still drives the ring and the graded status text; it simply no
longer answers a binary it was never designed for
A single exhausted resource was masked by an idle one. The breach test
ran on the average of the usage ratios, so memory at 104% of its allowance
beside CPU at 12% averaged to 58 and reported “within limits” — the dashboard
said everything was fine while a limit was being exceeded. A breach is now any
one resource over its own limit. Averaging remains correct for the graded
score and is kept there
Profiling required the Go toolchain at runtime, so it was dead in
production. The profile view shelled out to go tool pprof, and on any
distroless, scratch or alpine image — nearly every production deployment of
a Go service — go is not on PATH, so the dashboard showed
Error: 'go' command not found where the report should be. It also spawned a
subprocess per HTTP request. Profiles written by runtime/pprof are fully
symbolized, so they are now parsed in-process with
github.com/google/pprof/profile, which needs no binary, no source tree and
no toolchain. os/exec is gone from that path entirely
Annotated source (pprof -list) additionally needed the source tree the
binary was built from. The per-line sample counts come from the profile and
now always render; the source text is interleaved only where the file is
actually readable, and says so plainly where it is not
An empty profile rendered as an empty table, which reads as “this function
used no CPU”. It now explains that the profiler samples at 100Hz and a call
shorter than that usually records nothing
The OTel exporter reported success against an unreachable collector.Export only stored values; the SDK’s reader shipped them later on its own
clock, so the error never reached the caller and the dashboard would have
read “ok” while nothing arrived. Export now flushes and returns the
transport’s verdict, leaving one clock instead of two
WithOTelEndpoint connected to the collector and never sent a metric.Registry, Pipeline and MultiExporter were each implemented and each
unit-tested, and nothing ever constructed them, so OTelExporter.Export was
unreachable from a running service. The exporter was created, logged as
OTel exporter initialized, and shut down again without being asked for
anything in between; because its instruments are registered lazily on the
first Export, its periodic reader collected an empty set forever. Measured
against a real OTLP receiver: 0 metrics before, 5 within 30s after
Per-function call counts and approximate latency percentiles.CALLS,
P50 and P95 on the Functions table. The distribution comes from a
fixed-bucket histogram — 216 bytes per function regardless of call volume,
against ~10 MB at the function cap for keeping raw samples — so the values
are bucket upper bounds rather than interpolations, shown as ~4ms meaning
“at most 4ms”. Below 20 calls the columns read —: p95 of three calls is the
slowest of three, not a percentile
GET /metrics answered HTTP 500 under RegisterDashboardHandlers and 404
under Fiber, while the same path answered 200 under RegisterAPIHandlers.
The Prometheus endpoint is served at the root rather than beneath the API
base path, and the unified handlers dispatched to the API only on that
prefix, so the scrape endpoint fell through to the static file server. Any
consumer mounting the dashboard through RegisterDashboardHandlers or
RegisterSecuredDashboardHandlers — or through Fiber — had Prometheus
marking the target down
The API surface was declared separately in five registration sites, which is
how the above drifted. routes.go now declares it once and all five derive
from it, and a test drives every route through every registration style
A window predating service start returned HTTP 500. Both read endpoints
clamp the requested start up to the service start time, so a window ending
before startup became start > end, which the storage layer rejects. The
dashboard reads a failed series fetch as a lost connection, so asking for an
old range reported the service as having stopped answering
The two storage backends disagreed about an empty result: tstorage returned
an error where the in-memory backend returned an empty slice. Both now answer
(nil, nil)
pprof.StartCPUProfile’s error was discarded. CPU profiling is a
process-wide singleton, so a second traced call sampled concurrently got
“already in use”, the error vanished, and the caller recorded a profile path
pointing at the zero-byte file os.Create had just truncated — or at one the
other call was writing
The Reports range control offered a 5m button its renderer had no entry for;
clicking it threw out of the handler and silently stopped the page. Its LIVE
pill was permanently green and wired to nothing
The runtime chart never refreshed while LIVE was on, and the “not enough
history” message hardcoded a 5m sync interval regardless of configuration
tstorage data from a test run was committed to the repo, so running the
suite modified a tracked file and left the working tree dirty
One thing the design canvas shows that the dashboard deliberately does not,
because the data behind it does not exist yet. It is tracked rather than faked,
and the page is absent rather than filled with plausible numbers:
Exporters page — needs Prometheus and OTel status surfaced server-side (#67)
The dashboard rebuilt on the new design, and a set of measurements that
were reporting things they had not measured. The security fix that landed
alongside this work is in 1.4.1, released separately.
The dashboard was rebuilt on the new design. A 226px rail carrying the
service identity, navigation and storage footprint; a top bar with the page
title, window control, live indicator and theme toggle; and the failure states
the old dashboard had no answer for — an as of stamp, a staleness banner
naming the last good reading, and dimmed content so stale numbers cannot be
read as current
The embedded payload went from 7.9 MB to 352 KB. Every byte of it shipped
inside every binary importing this library and was downloaded by every
go get, whether or not the dashboard was ever opened. The vendored Bootstrap
template (4.3 MB) was held up by 26 class names and one custom scrollbar;
ECharts (1.0 MB) drew a single chart, now inline SVG; 1.8 MB of PNG tile icons
and a loading GIF were replaced by an icon sprite
The default theme is light, and the theme mechanism moved from a class to
body[data-theme]. Stored preferences carry over unchanged
GetCPUPrecent and GetCPULoad no longer block. Both called
cpu.Percent(time.Second, ...), which sleeps for a second to take two
samples, and GET /metrics reaches both — so every dashboard poll spent two
seconds in the handler before it could answer. 2.05s to 0.01s. The reading
is now taken once, non-blocking, and shared
common.LibraryVersion() reports the version compiled into the binary, read
from build info rather than declared as a constant. The dashboard shows it in
the rail. A constant is a promise nobody keeps: the footer claimed v1.0.0
for four releases after it stopped being true
GoroutineLeakReport.Groups and GroupsTotal carry every distinct call
stack, not only the offending ones. A breakdown by state cannot be computed
from the offenders alone
Guardrail tests for the invariants the front end has no other way to check:
undefined CSS custom properties (including in HTML style attributes),
self-referential tokens, tokens defined twice, hidden defeated by a
display rule, dead local page links, hardcoded versions, self-asserted
credentials, and the embedded payload budget
The sidebar gains the design’s shell: a service identity block (name, PID, Go version and a connection dot), a MONITOR section label above the navigation, live counts on the Function Metrics and Go Routines items, and a storage footer showing the backend, retention window and on-disk footprint. All of it is chrome — true of the instrument rather than of any one page — so every page shows it and no page fetches it twice
/service-info exposes retention_period, storage_type and storage_on_disk. storage_on_disk is omitted for the in-memory backend, since reporting 0 B there would read as “nothing stored” rather than “not applicable”
common.GetRetentionPeriodString() and timeseries.GetStorageType()
Traced-function memory was measured wrong, and reported when it had not been
measured at all. The delta came from runtime.MemStats.Alloc, which is bytes
currently live, so a collection during the call dropped it and the
difference went negative; that was guarded and otherwise reported as zero, so
a heavy allocator that triggered a GC recorded as allocating nothing. Using
TotalAlloc, which is cumulative, main.highMemoryUsage goes from a measured
0 to a measured 766.52 MB. Separately, profiling runs on one call in
SamplingRate (100 by default), so 99 calls in 100 had never been measured and
showed a confident 0; memory_usage_sampled now distinguishes the two and
the dashboard renders an em dash for the unmeasured case
The goroutine delta clamped negatives to zero, so a call that let goroutines
finish looked identical to one that did nothing. It is also process-wide
rather than per-function — any other goroutine starting or finishing during
the call moves it — which is now stated at the type rather than implied away
by the column heading
The function detail panel labelled values “cumulative”. Every FunctionMetrics
field is overwritten by each traced call, so they all describe the most recent
one
Staleness no longer decides whether goroutines are leaking. A listener
parked in netpoll and a signal handler waiting for SIGTERM are both
permanently blocked and both healthy, and every Go server has them, so a
staleness-driven verdict declared a leak on every process. The threshold had
been pushed to 24 hours to stop it firing, which removed the only signal it
carried. LeakSuspected now follows growth, which is what separates a leak
from a long wait; stale counts remain as context, and the threshold returns to
30 minutes
A function row could be opened but not closed. hidden works by applying
display: none from the UA stylesheet, which loses to every author rule, so
display: flex silently defeated it. The same rule made every unopened row
reserve 31px for an empty box
The health ring shaded itself on a threshold invented in the front end while
the text beside it used the server’s verdict, so a brown ring could sit next
to the word “Healthy”. The server holds the configured limits and is the only
thing that knows what healthy means for a service
Three light-theme components put coloured text on a tint of its own colour,
which holds on a dark ground and fails on a light one
The v2.0.0 tag remains unreachable and cannot be retracted. retract v2.0.0
in a module whose path has no /v2 suffix is itself rejected — “version
v2.0.0 invalid: should be v0 or v1, not v2” — so the same rule that stops the
proxy serving the tag stops it being formally withdrawn. It is inert either way:
go get @latest resolves to the newest v1, and go list -m -versions does not
list it. It survives only on the GitHub releases page.
Released on its own, ahead of the dashboard work, because none of it
depends on that and a credential the dashboard hands itself should not
wait behind a UI project.
The dashboard no longer asserts its own privilege.authenticatedFetch
attached X-User-Role: admin and a hardcoded monigo-admin-secret to every
request that carried no API key, both lifted from
example/security-examples/custom-auth, whose auth function grants access for
precisely those. A consumer following that documented example got a dashboard
that satisfied their own auth check on its own say-so. A browser cannot vouch
for itself; custom authentication belongs in the middleware
The API key moves from the query string to an X-API-Key header. As a query
parameter it was recorded in browser history, sent in the Referer to any
external link the page carried, and written to every access log in between.
APIKeyMiddleware already accepted both forms
Design token layer in static/css/monigo-styles.css: 122 custom properties covering type, spacing, radius, elevation, five surface planes, four ink tones, five semantic states and an eight-colour chart series palette, in matched light and dark sets. Purely additive — nothing consumes them yet, and removing both rules from the live stylesheet changes the computed style of zero of 713 sampled elements. Every documented contrast ratio is verified: text pairs clear 4.5:1, control borders and the focus ring clear 3:1
Type and density follow the design’s scale. Metric tiles are now an uppercase micro-label over one large tabular number, card titles and body text sit on a six-step scale topping out at 24px rather than the vendored template’s defaults, card padding is 16px, and table rows are tighter with a sticky header. Every font-size and border-radius in the project stylesheet now comes from a token: 12 ad-hoc sizes and 5 ad-hoc radii reduced to 6 and 3
Metric labels drop their trailing colons, since an uppercase eyebrow does not take one
The dashboard adopts the new design’s palette and collapses the dark theme onto it. Colours now come from the design system: a cyan accent (#00B4E6 dark / #00758F light) replacing the orange, five surface planes per theme, and five semantic states. Hex literals in real declarations drop from 105 to 2; body.dark-theme component rules from 53 to 3. Every text pair clears WCAG AA and every control boundary clears 3:1, verified in a running dashboard in both themes
Focus rings restored. The vendored .btn:focus { outline: none; box-shadow: none } stripped every focus indicator with no replacement, so tabbing the dashboard moved nothing visually
Stack traces and pprof output render in IBM Plex Mono at a fixed size on a recessed surface. The previous rule asked for 'Fira Code', which is not shipped and never resolved
Table numerics use tabular-nums, so live columns stop jittering on each poll
Cards, tables and muted text now read their colours from the design tokens rather than hardcoded hex literals. Colours shift slightly onto the corrected palette and every affected text pair gains contrast — dark muted text goes from 5.78:1 to 8.38:1, dark card text from 13.34:1 to 14.98:1
Tagged at a58a30f, a branch commit that was subsequently squash-merged, so
the tag is not an ancestor of main. Its contents are everything through the
design token layer — which is more than this section originally described, so
the entries for the offline dashboard, the memory-chart fixes, the payload
reduction and the asset guardrails have been folded in here rather than left
under Unreleased. Published versions are immutable on the module proxy, so the
tag stands; the record is corrected instead.
BasicAuthMiddleware and APIKeyMiddleware now compare credentials with crypto/subtle.ConstantTimeCompare — != short-circuits on the first differing byte and leaks length and prefix information to a timing oracle
ViewFunctionMetrics validates the function name and report type before invoking go tool pprof — a name beginning with - was interpreted as a flag by pprof, and the report type was interpolated straight into the argument list
BasicAuthMiddleware now sends a WWW-Authenticate challenge when a request carries no credentials at all
Silently truncated goroutine dumps: CollectGoRoutinesInfo used a fixed 1 MB buffer with a single runtime.Stack call. runtime.Stack truncates without reporting it, so an application with thousands of goroutines got a clipped trace and undercounted — precisely the situation goroutine leak detection needs to be accurate in. It now grows the buffer and retries, capped at 64 MB
Inverted health score: calculateServiceHealth clamped the score to 100 — the best possible value — when usage exceeded every threshold, so a fully degraded service reported as healthy. It now returns 0, matching calculateSystemHealth
Unbounded memory growth: InMemoryStorage.InsertRows ignored the retention period and never evicted, so WithStorageType("memory") grew for the life of the process
WithStorageType had no effect: the storage singleton was initialised by SetDataPointsSyncFrequency before SetStorageType was applied, so "memory" silently fell through to disk
Data race on serviceHealthThresholds between ConfigureServiceThresholds and the metrics collection goroutine
Panic in common.ConvertToMB on memory strings shorter than three characters, reachable from health calculation; calculateMemoryUsagePercentage also guards against a zero total
Panic in TraceFunctionWithArgs / TraceFunctionWithReturns when passed a nil argument — reflect.ValueOf(nil).Type() panics. Nillable parameter kinds now accept nil as the zero value
CloseStorage() was permanently terminal via sync.Once; a subsequent GetStorageInstance() returned the closed handle instead of re-initialising
Goroutine leak: each call to SetDataPointsSyncFrequency started a ticker goroutine without stopping the previous one
Goroutine leak: RateLimitMiddleware’s cleanup goroutine is now stopped by Shutdown(), not only by the caller invoking the returned stop
File descriptor leak in WriteHeapProfile; nil check in StopCPUProfile
registry.GetAll() documented a snapshot copy but shared the Labels map with the live registry
The dashboard no longer requires internet access. Font Awesome 4.7.0 and html2canvas were loaded from cdnjs, and the vendored template CSS pulled a Lato webfont from Google Fonts, so on an airgapped host — where a lot of production Go services run — icons were blank boxes and the screenshot feature was dead. All three fetches are gone: icons are an inline SVG sprite, html2canvas is vendored, and the font imports are removed
Mobile navigation was impossible in every network condition. The sidebar and navbar menu buttons were <i> elements carrying las la-bars and ri-menu-* classes — Line Awesome and Remix Icon — and neither font was ever loaded: font-family: remixicon appeared in the vendored CSS with no @font-face rule and no font file anywhere in static/. They rendered as zero-size empty elements, so the sidebar could not be opened below 1300px and the navbar below 992px. Both are now real 22px controls
Removed static/css/core/intro.css (192 KB), referenced by no page and carrying a third Google Fonts import
Memory Distribution pie plotted values of different magnitudes against each other. The chart parsed a number out of the pre-formatted display strings ("2.68 MB", "12.93 GB"), which discards the unit — so a service using 2.68 MB rendered as 14.3% of the pie instead of 0.02%, a ~700x overstatement of its footprint
Heap Memory Usage chart mixed units under an “MB” axis. It read mem_stats_records[].record_value, a display number whose unit lives in a separate record_unit field that the chart ignored. Once heap use crosses 1 GB, HeapSys reports in GB while HeapAlloc is still in MB, and the taller bar renders shorter. It now reads raw_mem_stats_records, which is in one consistent unit
CPU Statistics pie plotted total_cores as a slice alongside its own components, so the chart always summed to twice the real core count. The third slice is now idle cores
Goroutine leak detection: every collection cycle evaluates all goroutine stacks for stale goroutines (blocked past a threshold, default 24h, configurable via WithStaleGoroutineThreshold) and for call stacks growing monotonically across the last 5 cycles. The verdict is carried on GoRoutinesStatistic.LeakReport and ServiceStats.GoroutineLeakReport, surfaced as a warning panel on the Go Routines Stats page, and raises the alert webhook when configured
core.AnalyzeGoroutineLeaks(), core.SetStaleGoroutineThreshold(), core.GetStaleGoroutineThreshold(), and core.ResetLeakDetectionState()
Health breach webhook alerts via WithAlertWebhook(url) — an opt-in POST fired when the system or service health score drops below 70. Dispatch is off the collection path, requests time out after 10 seconds, and deliveries are rate limited to one per 5 minutes across both alert types. Build() rejects a URL that is not http:// or https://
common.GetServiceName() — accessor for the configured service name
core.GetThresholds() — thread-safe accessor for the configured health thresholds
Build() validates MaxCPUUsage and MaxMemoryUsage are within 0-100 and MaxGoRoutines is non-negative
Regression tests for every fix above
Unformatted memory values on the metrics API: total_system_memory_bytes, memory_used_by_system_bytes, memory_used_by_service_bytes, available_memory_bytes, stack_memory_usage_bytes, gc_pause_duration_ms. Additive — the existing formatted string fields are unchanged. Anything doing arithmetic on or plotting these metrics must use the raw fields, since the strings carry a unit suffix
monigo.gif re-encoded from 65 MB to 24 MB — the module zip is downloaded on every go get
Argument marshalling shared between TraceFunctionWithArgs and TraceFunctionWithReturns instead of duplicated
The embedded dashboard shrank from 17.1 MB to 7.9 MB (-54%), and from 71 files to 46. Everything under static/ is compiled into the consuming service’s binary by //go:embed static/* and downloaded by every go get, so this is weight every user carried whether or not they ever opened the dashboard. Removed: assets/ss/d1-d10.png (7.6 MB), Product Hunt marketing images, an unused animated logo and dropdown arrow, and a favicon variant set — 28 files, none referenced by any page, stylesheet, script or document. Note this does not shrink existing clones, since the blobs remain in git history; the module zip is what go get downloads
These versions exist as git tags and are described in the library’s
changelog, but the Go module proxy will not serve them, so go get
cannot resolve them. They are kept here as a record. Nothing in this
section is installable.
Never published. This release was tagged v2.0.0, but the Go module proxy
rejects it: go.mod declares module github.com/iyashjayesh/monigo with no
/v2 suffix, and Go requires the major version to appear in the module path
from v2 onwards. go get therefore continued to serve v1.2.0, and everything
below was unreachable to anyone installing the library.
not found: invalid version: module contains a go.mod file, so module path must
match major version ("github.com/iyashjayesh/monigo/v2")
The release line was renumbered rather than renaming the module: /v2 would be
permanent for an API that is not v2-shaped, and per the proxy there were no v2
consumers to preserve. The changes below shipped in 1.3.0.