Project Research Summary¶
Historical research — not current guidance. Recorded before implementation was finalized; some recommendations here were never implemented. For current configuration and behavior, see the Operator Guide.
Project: cee-exporter v2.0 — Operations & Output Expansion Domain: Go audit-event bridge daemon — ops instrumentation + SIEM output writers Researched: 2026-03-03 Confidence: HIGH (architecture derived from direct codebase inspection; library choices verified via pkg.go.dev official docs)
Executive Summary¶
cee-exporter v2.0 extends a mature, well-structured Go daemon by adding 6 new capabilities across two categories: operational readiness (Prometheus metrics, systemd unit, Windows Service) and SIEM output expansion (SyslogWriter, BeatsWriter, BinaryEvtxWriter). The existing v1 architecture — with its clean Writer interface, pkg/metrics atomic counters, and pkg/queue worker pool — is well-suited for all 6 features. None require architectural rewrites; each slots into established extension points. The 4-phase build order recommended by research (Prometheus+Systemd, Windows Service, Syslog+Beats, BinaryEvtxWriter) reflects genuine dependency ordering and risk management, not arbitrary sequencing.
The library selection is minimal and defensible. Only 3 new external dependencies are introduced: prometheus/client_golang@v1.23.2 (industry standard, pure Go), elastic/go-lumber@v0.1.1 (official Elastic Lumberjack v2 client), and crewjam/rfc5424@v0.1.0 (minimal cross-platform RFC 5424 formatter). The Windows Service wrapping reuses golang.org/x/sys/windows/svc already present in go.mod via the kardianos/service abstraction. The systemd unit file is a pure text artifact requiring zero code changes. All features maintain the CGO_ENABLED=0 constraint for fully static cross-compiled binaries.
BinaryEvtxWriter is the single highest-risk item in v2.0. No pure-Go EVTX writer library exists; the implementation must be built from scratch against the MS-EVEN6 binary specification, with an estimated scope of 600–1200 LOC spread across a dedicated pkg/evtx/binxml/ subpackage. This feature must be isolated in its own phase, developed against a round-trip test fixture, and must not block delivery of the other 5 features. The remaining 27 documented pitfalls are concrete and preventable — most reduce to applying patterns already established in GELFWriter (mutex-protected reconnect, single source of truth for counters) to the new writers.
Key Findings¶
Recommended Stack¶
The v1 stack requires only 3 net-new external dependencies. All are pure Go, all build with CGO_ENABLED=0, all cross-compile from Linux to Windows.
See full analysis: .planning/research/STACK.md
Core technologies:
github.com/prometheus/client_golangv1.23.2 — Prometheus counter/gauge registration and HTTP handler; only officially backed Go Prometheus library; separate9xxxport requiredgithub.com/elastic/go-lumberv0.1.1 — Lumberjack v2 wire protocol client for Logstash/Graylog Beats Input; official Elastic library; protocol is frozen (stable since Beats 6.x)github.com/crewjam/rfc5424v0.1.0 — RFC 5424 message construction writing toio.Writer; minimal, cross-platform; avoids stdliblog/syslogwhich fails to build on Windowsgolang.org/x/sys/windows/svc(already in go.mod) — Windows SCM service registration, start/stop lifecycle; no new module requiredgithub.com/kardianos/servicev1.x — cross-platform service wrapper that delegates tox/sys/windows/svcon Windows and is a no-op shim on Linux; July 2025 release- stdlib (
encoding/binary,hash/crc32,unicode/utf16) — BinaryEvtxWriter implementation; zero new external dependencies for the highest-complexity feature
Critical version constraint: prometheus/client_golang v1.23.2+ required to avoid the go.uber.org/atomic/grafana/regexp transitive dependency that broke CGO_ENABLED=0 in earlier versions.
Expected Features¶
See full analysis: .planning/research/FEATURES.md
Must have (table stakes) — operators cannot deploy without these:
- Prometheus
/metricsendpoint — standard ops contract for any Go daemon in 2026; atomic counters already exist inpkg/metrics, exposure is 60–80 LOC; usecee_events_received_total,cee_events_dropped_total,cee_queue_depth,cee_writer_errors_total - Systemd unit file — Linux production deployment without a unit file is not repeatable; pure text artifact; use
Type=simple(notType=notifyunlesssd_notifyis wired in); zero code changes - Windows Service registration — SCM-managed service is the Windows production contract; NSSM is unacceptable in production;
svc/mgralready imported; exposeinstall/uninstall/start/stopsubcommands - SyslogWriter (RFC 5424) — every SIEM speaks syslog; structured data (SD-ELEMENT) carries all audit fields; broadest compatibility target matrix
Should have (differentiators) — justify the v2.0 release:
- BeatsWriter (Lumberjack v2) — direct ingestion by Logstash and Graylog Beats Input without a sidecar agent; ECS field alignment for Kibana/Elastic compatibility
- BinaryEvtxWriter (pure-Go BinXML) — native
.evtxfiles readable by Event Viewer and forensics tools on Linux; closes the only remaining cross-platform EVTX gap from v1
Defer (anti-features — do not build in v2.0):
- MSI installer for Windows Service — ship
cee-exporter.exe installCLI subcommand instead - Prometheus push gateway — pull model is correct for a long-running daemon
- RFC 3164 legacy BSD syslog — unstructured; cannot carry audit fields reliably
- Grafana dashboards / alerting rules — operational concern, document recommended PromQL queries instead
- NSSM wrapper — external binary, breaks single-artifact deploy contract
Architecture Approach¶
The v2 integration adds components at four well-defined extension points without touching the core event pipeline (server → queue → parser → mapper → writer). The CEPA handler, queue, parser, mapper, and pkg/metrics are all unchanged. See full diagram: .planning/research/ARCHITECTURE.md
Major components added in v2:
pkg/prometheus/handler.go— standalone HTTP server on port 9228; readspkg/metrics.Matomics viaSnapshot()and exposes them as Prometheus metrics using a privateprometheus.NewRegistry()(notDefaultRegisterer); runs in its own goroutine independent of the CEPA muxpkg/evtx/writer_beats.go— implementsWriterinterface; wrapsgo-lumberSyncClientwith async.Mutex; mirrorsGELFWriterreconnect pattern; mapsWindowsEventfields to ECS-alignedmap[string]interface{}pkg/evtx/writer_syslog.go— implementsWriterinterface; usescrewjam/rfc5424for message construction; sends via rawnet.Conn(UDP or TCP); no build tags — all platformspkg/evtx/writer_binary_evtx.go+pkg/evtx/binxml/subpackage — replaces stub; writes EVTX file header + chunks + BinXML token streams; build tag!windows; stdlib onlycmd/cee-exporter/service_windows.go+service_notwindows.go—kardianos/servicewrapper;main()split intomain()+run(); Windows-only service registration exposed via-service install|uninstall|start|stopflagsdeploy/systemd/cee-exporter.service— static file, no Go code
Key architectural decision — separate metrics port: The Prometheus /metrics endpoint must NOT share the CEPA mux on port 12228. The CEPA mux may be TLS-only; Prometheus scrapes would need TLS client configuration. A separate port (9228 default) with its own http.ServeMux avoids scrape log pollution in CEPA logs and mux method conflicts. This is the industry standard for Go exporters.
Config additions (go.mod + config.toml schema):
[metrics] enabled, addr (0.0.0.0:9228)
[output] beats_host, beats_port, beats_tls
syslog_host, syslog_port, syslog_protocol, syslog_tls
New type values: "beats", "syslog" (adds to existing "gelf", "evtx", "multi").
Critical Pitfalls¶
27 pitfalls documented across 6 feature areas. See full analysis: .planning/research/PITFALLS.md
Top 5 by consequence severity:
-
Prometheus mux collision (CRITICAL) — Registering
/metricsonDefaultServeMuxor on the CEPA mux causes 404/405 scrape failures or a port-binding race at startup. Prevention: dedicated port 9228, privateprometheus.NewRegistry(), separatehttp.ServeMux. -
BinaryEvtxWriter dual chunk checksum (CRITICAL) — EVTX chunks have two independent CRC32 fields (event records checksum at offset 52, header checksum at offset 124) computed over different byte ranges in a specific order. If either is wrong or confused, Event Viewer rejects the file as corrupt with no useful error message. Prevention: compute event records CRC first, write it at offset 52, then compute header CRC covering the already-updated chunk header.
-
Windows Service SCM 30-second startup timeout (CRITICAL) — The SCM clock starts at
CreateProcess; Go runtime init + config loading + TLS setup beforesvc.Run()can breach 30 seconds on boot-loaded VMs. Prevention: sendStartPendingas the very first action inExecute(); useStartAutoDelayedservice type (120-second window) instead ofStartAutomatic. -
SyslogWriter log/syslog Windows build failure (CRITICAL) — stdlib
log/sysloghas//go:build !windowsembedded; importing it breaks the Windows cross-compile. Prevention: usecrewjam/rfc5424+net.Conn— no platform build constraints anywhere in the dependency. -
BeatsWriter SyncClient not thread-safe (HIGH) —
go-lumberSyncClienthas no internal locking; the existing queue runs N concurrent workers. ConcurrentSend()calls corrupt the Lumberjack framing state. Prevention: wrapSyncClient.Send()in async.MutexinsideBeatsWriter.WriteEvent()— the same patternGELFWriteralready uses.
Additional high-impact pitfalls to address per phase:
- Prometheus: cardinality explosion if
ObjectName/SubjectUsernameused as metric labels (use onlyevent_id,writer,status) - Prometheus: parallel counter drift if
pkg/metrics.Storeatomics andprometheus.Counterare incremented independently (wrap, don't duplicate) - BinaryEvtxWriter: record trailing size copy missing causes entire chunk to be unreadable
- BinaryEvtxWriter: string table offsets are chunk-relative, not file-relative
- BinaryEvtxWriter: stub/real type name conflict — remove
writer_evtx_stub.gobefore adding real implementation - BeatsWriter: TLS silently dropped on reconnect if
SyncDialused instead of customdialFunc - Windows Service:
os/signal.Notify(SIGTERM)conflicts with SCM control handler; gate behind!svc.IsWindowsService()
Implications for Roadmap¶
Based on research, the 4-phase structure below reflects actual dependency ordering and risk progression. All phases after Phase 1 are independent of each other except where noted.
Phase 1: Observability Foundation¶
Rationale: Prometheus metrics and the systemd unit file are the two lowest-risk, highest-ops-value items. Prometheus can be built and tested entirely on Linux without a Windows environment. The systemd unit file is a zero-code text artifact. Together they constitute the operational readiness baseline that makes the daemon deployable in managed Linux environments. This phase also includes the run() extraction refactor in main.go (splitting main() into main() + run()), which is a prerequisite for Phase 2 Windows Service integration.
Delivers:
/metricsendpoint on port 9228 with 4 Prometheus metrics (cee_events_received_total,cee_events_dropped_total,cee_queue_depth,cee_writer_errors_total)deploy/systemd/cee-exporter.service— production-ready hardened unit filemain()→run()refactor (prerequisite for Phase 2)go.modaddition:prometheus/client_golang@v1.23.2
Uses: github.com/prometheus/client_golang v1.23.2 (new), golang.org/x/sys (existing)
Implements: pkg/prometheus/handler.go (new), deploy/systemd/cee-exporter.service (new), cmd/cee-exporter/main.go (modified)
Avoids: Pitfall 1 (mux collision), Pitfall 2 (blocking Collect), Pitfall 3 (cardinality explosion), Pitfall 18 (missing network-online.target), Pitfall 20 (Type=forking for Go binary), Pitfall 23 (parallel counter drift)
Research flag: Standard patterns. Skip research-phase — Prometheus instrumentation and systemd unit files are exhaustively documented.
Phase 2: Windows Service¶
Rationale: Depends on the run() extraction from Phase 1. Uses golang.org/x/sys/windows/svc already in go.mod — no new module unless kardianos/service is chosen for the cross-platform abstraction. Medium complexity (~150–200 LOC). Must be developed and tested in a Windows environment (or Windows CI). Isolated in service_windows.go / service_notwindows.go so it cannot affect Linux builds.
Delivers:
cee-exporter.exe install/uninstall/start/stop/statusCLI subcommands- SCM-managed service with Automatic Delayed Start and restart recovery actions
- Windows Event Log start/stop entries
go.modaddition:github.com/kardianos/service(if chosen over directx/sys/windows/svc)
Uses: golang.org/x/sys/windows/svc (already in go.mod), optionally github.com/kardianos/service
Implements: cmd/cee-exporter/service_windows.go (new), cmd/cee-exporter/service_notwindows.go (new), cmd/cee-exporter/main.go (modified — add -service flag)
Avoids: Pitfall 4 (30-second SCM startup timeout — send StartPending first), Pitfall 5 (SIGTERM/SCM conflict — gate os/signal.Notify behind !IsWindowsService()), Pitfall 6 (panic in Execute — wrap body in defer recover()), Pitfall 24 (NSSM dependency — use native svc.Run())
Research flag: Needs review of kardianos/service vs direct x/sys/windows/svc API. The architecture research recommends kardianos/service for the clean abstraction; the stack research recommends direct x/sys. Resolve by inspecting kardianos/service source to confirm it delegates to x/sys with no overhead. Either choice is valid; decide before coding.
Phase 3: SIEM Writers (Syslog + Beats)¶
Rationale: SyslogWriter and BeatsWriter are independent of each other and of Phase 2. Both implement the existing Writer interface at well-defined extension points. Both can be developed and tested on Linux. SyslogWriter should be built first because it has slightly lower risk (no ACK windowing protocol) and broader SIEM compatibility. BeatsWriter second because it requires a running Logstash or Graylog Beats Input for integration testing. Parallel development is possible if two engineers are available.
Delivers:
SyslogWriter: RFC 5424 structured syslog over UDP/TCP to any syslog server; all audit fields in SD-ELEMENT; no Windows build constraintsBeatsWriter: Lumberjack v2 client sending to Logstash/Graylog Beats Input; TLS support; ECS-aligned field namesgo.modadditions:crewjam/rfc5424@v0.1.0(SyslogWriter),elastic/go-lumber@v0.1.1(BeatsWriter)- New config fields:
syslog_*,beats_*in[output]section - New
typevalues:"syslog","beats"inbuildWriter()
Uses: github.com/crewjam/rfc5424 v0.1.0, github.com/elastic/go-lumber v0.1.1, stdlib net
Implements: pkg/evtx/writer_syslog.go (new), pkg/evtx/writer_beats.go (new), main.go buildWriter() (modified), config.toml.example (modified)
Avoids:
- Pitfall 11 (AsyncClient callback deadlock — use SyncClient)
- Pitfall 12 (SyncClient not thread-safe — sync.Mutex, mirror GELFWriter)
- Pitfall 13 (TLS dropped on reconnect — use custom dialFunc for both initial and reconnect paths)
- Pitfall 14 (log/syslog Windows build failure — use crewjam/rfc5424 which has no platform constraints)
- Pitfall 15 (stdlib syslog not RFC 5424 compliant — use library, not stdlib)
- Pitfall 16 (nil pointer on closed UDP conn — hold mutex for close-and-nil sequence)
- Pitfall 17 (unescaped backslash in SD-PARAM values — use library with built-in escaping)
- Pitfall 22 (MultiWriter masks per-writer failures — add
writerlabel to error counter) - Pitfall 26 (BeatsWriter missing read timeout — set
v2.Timeout(5s))
Research flag: SyslogWriter uses a known, stable pattern. BeatsWriter is MEDIUM confidence — the go-lumber API is official but the library has not been updated since 2021. Verify SyncDial vs SyncDialWith TLS pattern by reading go-lumber source before coding the reconnect path.
Phase 4: BinaryEvtxWriter¶
Rationale: Isolated last because it is the highest-complexity feature in v2.0, has no external library, requires the deepest format knowledge, and is entirely independent of all other phases. Blocking other deliverables on this would be wrong — the other 5 features provide immediate production value. The BinXML implementation should be built incrementally: file header first, then chunk header with checksums, then a minimal BinXML token stream for a single EventID, validated at each step against the 0xrawsec/golang-evtx parser before proceeding.
Delivers:
- Real
BinaryEvtxWriterreplacing the stub inwriter_evtx_stub.go - Valid
.evtxfiles readable by Windows Event Viewer, Splunk, Elastic Agent, Autopsy pkg/evtx/binxml/subpackage:chunk.go,header.go,token.go,sid.go,filetime.go- Round-trip test suite using
0xrawsec/golang-evtxas a test-only parse dependency - Zero new external runtime dependencies (stdlib only)
Uses: encoding/binary, hash/crc32, unicode/utf16, bytes, os, sync (all stdlib)
Implements: pkg/evtx/writer_binary_evtx.go //go:build !windows (new, replaces stub), pkg/evtx/binxml/*.go (new subpackage), pkg/evtx/writer_evtx_stub.go (removed or tagged //go:build ignore)
Avoids:
- Pitfall 7 (dual chunk checksum — compute event records CRC first at offset 52, then header CRC at offset 124)
- Pitfall 8 (record trailing size copy — buffer record in bytes.Buffer, append size copy before write)
- Pitfall 9 (string table offsets are chunk-relative — maintain chunk-local intern map, reset per chunk)
- Pitfall 10 (stub/real symbol collision — remove or ignore the stub file before adding real implementation)
- Pitfall 25 (chunk boundary overflow — check bytesUsed + recordSize before appending each record)
Research flag: NEEDS deeper research during planning. The BinXML token encoding (especially NormalSubstitution, TemplateInstance tokens, SID binary encoding, FILETIME encoding) requires working through the MS-EVEN6 specification in detail before writing code. Recommend a research-phase task specifically for BinXML token format and a spike implementation that writes a single valid event record and validates it before the full implementation begins.
Phase Ordering Rationale¶
- Phase 1 before Phase 2: The
main()→run()refactor in Phase 1 is a prerequisite for therunWithServiceManager(run)pattern in Phase 2. If done out of order, Phase 2 would need to refactormain.goitself, creating unnecessary merge conflict surface. - Phases 3 and 4 after Phase 1: Both add to
buildWriter()inmain.go. Having the Prometheus-refactoredmain.goas a stable base before adding writer cases reduces churn. - Phase 4 last: BinaryEvtxWriter is independent of all other phases technically, but its complexity warrants isolation at the end when the codebase is stable and the writer infrastructure is proven.
- Phase 3 writers parallel-capable:
writer_syslog.goandwriter_beats.gohave zero dependency on each other. A second engineer can start BeatsWriter while the first completes SyslogWriter. - Systemd unit in Phase 1 (not standalone): Shipping it with Prometheus ensures the unit file's
ExecStartpath and environment variables align with the same binary that exposes/metrics. They belong together operationally.
Research Flags¶
Needs deeper research during planning (research-phase recommended):
- Phase 4 (BinaryEvtxWriter): BinXML token encoding (TemplateInstance, NormalSubstitution), SID binary format, FILETIME encoding, and string table management require format-spec-level research before coding. Strongly recommend a spike implementation validated against
wevtutil qeor the Rustomerbenamram/evtxcrate before committing to the full implementation. - Phase 2 (Windows Service): Resolve
kardianos/servicevs directx/sys/windows/svcAPI choice. Both research files give different recommendations; this needs a 30-minute review of kardianos/service source to determine if the abstraction adds value for this use case.
Standard patterns — skip research-phase:
- Phase 1 (Prometheus): Official
prometheus/client_golangdocs are exhaustive; the Collector pattern for wrapping atomics is well-established. - Phase 1 (Systemd): Unit file is a known artifact; patterns verified against current systemd man pages.
- Phase 3 (SyslogWriter):
crewjam/rfc5424API is minimal; RFC 5424 is a stable spec. - Phase 3 (BeatsWriter):
go-lumberSyncClientAPI is straightforward; verify TLS path from source before coding (30 minutes, not a full research phase).
Confidence Assessment¶
| Area | Confidence | Notes |
|---|---|---|
| Stack | HIGH | All library choices verified via pkg.go.dev official docs; versions confirmed from GitHub releases pages; CGO_ENABLED=0 compatibility confirmed per source/issue reports |
| Features | HIGH | Feature expectations derived from production Go daemon conventions; table stakes list is consensus among Prometheus, systemd, and Windows service documentation |
| Architecture | HIGH | Derived directly from reading the v1 codebase; integration points are unambiguous; extension patterns already established by GELFWriter and Win32Writer |
| Pitfalls | HIGH | 27 pitfalls derived from: codebase inspection, official library docs, confirmed Go issue reports (golang/go#23479, golang/go#66666), and libevtx format specification |
Overall confidence: HIGH
Gaps to Address¶
-
go-lumber TLS reconnect path: The exact API for TLS-aware reconnect (
SyncDialWithvsNewSyncClientWithConn) is not definitively confirmed from documentation alone. Read go-lumber source for theclient/v2package before implementingBeatsWriter.connect(). Risk: LOW (affects one function in one writer). -
kardianos/service vs direct x/sys/windows/svc: The two research files give conflicting recommendations. STACK.md recommends direct
x/sys/windows/svc; ARCHITECTURE.md recommendskardianos/service. Resolution: if the kardianos/service abstraction cleanly delegates tox/syswithout reimplementing SCM protocol, prefer it for the simpler API. If it adds behavior that conflicts with the existingx/sysEventLog writer, use directx/sys. This is a 30-minute decision, not a blocker. -
BinaryEvtxWriter scope estimate: The 600–1200 LOC estimate is based on format specification review, not a working implementation. The actual scope may differ significantly. A spike implementation (targeting a single valid EventID 4663 record) should be the first deliverable in Phase 4 to validate the estimate before full implementation is committed.
-
BeatsWriter go-lumber maintenance status:
elastic/go-lumberhas not been updated since January 2021. The Lumberjack v2 protocol is frozen, so library staleness is not a functional risk. However, any Go toolchain compatibility issues (e.g.,go vetchanges, new linter rules) would require a fork. Log this as a low-priority maintenance note.
Sources¶
Primary (HIGH confidence)¶
prometheus/client_golangGitHub releases / pkg.go.dev — v1.23.2 confirmed 2025-09-05; Collector interface, custom registry, promhttp.HandlerForgolang.org/x/sys/windows/svcpkg.go.dev — v0.31.0; Execute method, service state machine, svc/mgr CreateService APIelastic/go-lumberclient/v2 pkg.go.dev — SyncClient.Send(), AsyncClient, TLS dial options, callback deadlock warningcrewjam/rfc5424pkg.go.dev — v0.1.0; Message type, WriteTo(io.Writer) API; pure Go confirmedkardianos/servicepkg.go.dev — v1.x; Interface (Start/Stop/Run), July 2025 release confirmed- libyal/libevtx EVTX format specification — chunk header layout, dual checksums, string table offsets, BinXML token vocabulary
0xrawsec/golang-evtxsource — ChunkHeader struct; confirmed read-only parser (no write capability)golang/go#23479— 30-second SCM startup timeout root cause confirmedgolang/go#66666—log/syslogmissing RFC 5424 VERSION field; Windows build exclusion confirmed- RFC 5424 (IETF datatracker) — STRUCTURED-DATA escaping rules (
],",\), SD-ID uniqueness constraint - systemd.service man page — Type=, After=, ExecStart absolute path requirement, network-online.target
- Direct codebase inspection:
main.go,writer_gelf.go,writer.go,metrics.go,queue.go,server.go,health.go
Secondary (MEDIUM confidence)¶
nathanaelle/syslog5424pkg.go.dev — confirmed UDP exclusion by design (informs library rejection)elastic/go-lumberGitHub (v0.1.1, 56 stars, CI passes) — stability assessment for unmaintained library- MS-EVEN6 BinXML specification (learn.microsoft.com) — BinXML token byte assignments; informative (not normative for implementation details below token level)
coreos/go-systemd/daemonpkg.go.dev — optional sd_notify READY=1 integration forType=notify
Tertiary (LOW confidence / deferred validation)¶
- BinaryEvtxWriter LOC estimate (600–1200) — based on format spec review; validate with spike implementation in Phase 4
- go-lumber TLS reconnect exact API — confirm from source before BeatsWriter implementation
Research completed: 2026-03-03 Ready for roadmap: yes