Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

166 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bugler

CI Licence: Apache 2.0

Self-hosted telemetry server: logs and traces in over OTLP, an explore UI out, and a mail or a Google Chat message to whoever subscribed when a service starts logging trouble. One container, one PostgreSQL, no agent to install.

Built for the situation where a company runs several applications, each deployed separately for several clients, and a team member may only see the telemetry of the applications they were granted.

The log list, with the volume chart above it and a release marked on it

What it does

  • Takes OTLP as it comes — logs and traces, gRPC on :4317 and HTTP on :4318, from any OpenTelemetry SDK or collector. A Service authenticates with its own API key, and that key is what decides whose telemetry this is — never what the payload claims about itself (ADR 0006).
  • Explore without a query language. Narrow by application, namespace, environment, service, severity, message or any attribute; the volume chart above the list answers "since when" before you have finished reading the first page, and marks the deployments that happened in the window. Follow keeps the newest at the top.
  • Traces that stay attached to their logs. A waterfall per trace, every span with its attributes and events, and one click from a span to the log records written inside it — or from a log to the trace it belongs to.
  • An unattended watch. Bugler polls what it stored, groups the same kind of trouble into an Episode rather than mailing every line, closes it after a quiet window, and mails or posts to Google Chat. Episodes can be acknowledged and solved, so a team can see who has it.
  • Access granted per application. Local accounts, no identity provider required; the first account created becomes the administrator. A member sees exactly the applications they were granted, and nothing tells them the rest exist.
  • Retention, per service, in days — separately for logs and for traces, with a storage ledger that says what each service costs today and what it will settle at.
  • Runs as one container. No sidecar, no message broker, no object storage: a .NET 10 modular monolith serving the UI, the REST API and both OTLP surfaces, over a single PostgreSQL.

Metrics have no receiver yet; they are planned for a later phase.

One log, and everything around it

A log record selected, with its attributes, resource and a way into its trace

A selected record shows what the sender attached to it — its own attributes, the resource that declared it, the scope that wrote it — and, when it was written inside a span, the way into the trace it belongs to.

Traces, and back again

The trace list: root span, duration, span count and status

One line per trace, with the slow ones marked in passing.

A trace waterfall with a failing span selected

The waterfall, the failing span, its attributes and the exception event exactly as it arrived — and View correlated logs, which is the same journey as the previous screen in the other direction.

Trouble, grouped and answerable

The Episodes page, with one open episode and its detail

An Episode is one kind of trouble in one Service — not one log line. It records what opened it, at which release, how loud it has been since, and who acknowledged it.

Administration

Admin: applications, services, retention and API keys

Applications and their Services, retention for each, alerting sensitivity and quiet windows, health checks, and the API keys — issued once and shown once.

Admin: the storage ledger, per service

What each Service's telemetry costs today, how fast it is growing, and what it will settle at once its retention starts throwing the oldest away. Estimates are marked as estimates.

Admin: users and their per-application grants

Who may read what, as a matrix. Administrators are never scoped; everybody else is.

Running it

docker compose up --build -d

This raises everything Bugler talks to — a PostgreSQL, and a mailpit that swallows every message so nothing reaches a real inbox. It is meant for a laptop. On a server both of those already exist and belong to somebody else: see DEPLOYMENT.md.

Open http://localhost:8080 — the first account created becomes the server administrator. Register an application and a service in Admin, issue an API key, and point your services at the server:

OTEL_EXPORTER_OTLP_ENDPOINT=http://your-server:4317
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer blgr_..."

See Sending telemetry for the details, and for the two mistakes that make an exporter drop everything without saying a word.

Nothing to send yet? tools/Bugler.SampleSource simulates a small e-shop — traces with correlated logs, a steady trickle of failures and a rarer one — which is what fills the screens above.

Shape

  • Single-process modular monolith (.NET 10, ASP.NET Core): OTLP ingest (gRPC :4317, HTTP :4318), REST API, and the frontend served from one container.
  • PostgreSQL as the only backing store (telemetry + catalog + users).
  • React + TypeScript frontend (Bun toolchain, shadcn/ui, TanStack Router/Query).
  • Distributed via Docker + docker-compose.

Domain

The domain hierarchy is Application → Service → Tenant:

  • An Application is a product; user access is granted per application.
  • A Service is one registered sender — one role of one deployment, identified by its namespace, environment and name (demo/prod · backend). It owns its API keys and retention, and its identity is what the key proves, never what the telemetry claims about itself (ADR 0006).
  • A Tenant is a customer inside a multi-tenant Service, visible only as a filter attribute in telemetry.

The codebase is split into five bounded contexts — Ingestion, Exploration, Alerting, Registry, Access — described in CONTEXT-MAP.md, each with its own glossary (src/Bugler.<Context>/CONTEXT.md). Architectural decisions are recorded as ADRs in docs/adr.

Mail

Bugler mails two things: alerts to whoever subscribed, and password-reset links. Without SMTP both stay quietly off — Bugler runs fine, but alerts reach inboxes never and passwords cannot be reset by link.

Configure it while running, in Admin → Server: server (hostname or IP), port, security mode, credentials if the relay wants any, and the From address. Saving applies to the very next mail — no restart. The same screen sends a test message to your own account address and reports what the SMTP server actually said; use it, because a relay that refuses Bugler otherwise surfaces only in the container log.

A bare internal relay is a first-class citizen: an IP for the server, security None, credentials empty. The security modes:

Mode Meaning
Automatic STARTTLS when the server offers it, plaintext when it does not — the default
None plaintext on purpose, even if the server advertises STARTTLS
StartTls STARTTLS or the send fails — refuses to downgrade
ImplicitTls TLS from the first byte — the dedicated-port style, usually 465

Settings saved on the screen live in the database and win whole — never field by field — over the Mail:Smtp configuration section from the first save until the screen's Reset to server configuration; the screen always says which side is live (ADR 0014). A deployment that keeps SMTP in the environment keeps working unchanged; these matter only while nothing was ever saved in the UI:

Mail__Smtp__Host: "smtp.example.com"  # empty = mail disabled
Mail__Smtp__Port: "587"
Mail__Smtp__Security: "Automatic"     # Automatic | None | StartTls | ImplicitTls
Mail__Smtp__Username: ""              # empty = no authentication
Mail__Smtp__Password: ""
Mail__Smtp__From: "bugler@example.com"

The SMTP password is write-only: once saved it is never shown again, only replaced or removed. The docker compose of the previous section ships a mailpit that swallows everything Bugler sends — read it at http://localhost:8025.

Sending telemetry

Bugler is a plain OTLP endpoint. Anything that speaks the protocol can export to it — an OpenTelemetry SDK in any language, a logging-library sink such as Serilog.Sinks.OpenTelemetry or the OpenTelemetry Logback appender, or a Collector forwarding on your behalf. Bugler is not involved in the choice.

OTLP/gRPC :4317 — one address for every signal
OTLP/HTTP :4318POST /v1/logs, POST /v1/traces
Signals logs and traces; metrics are not implemented yet
Authentication Authorization: Bearer blgr_… on every export
Body protobuf only — JSON-encoded OTLP is refused with 415

Those are the ports the server itself listens on, as plain HTTP. Behind a reverse proxy the address senders are given is usually the hostname with no port at all, the proxy routing /v1/logs, /v1/traces and the gRPC service paths to them — see DEPLOYMENT.md.

Register an Application and a Service in Admin and issue that Service an API key. The key is the sender's identity: service.name and the rest of the resource attributes are stored and shown, but never decide which Service the data belongs to (ADR 0006). One key per deployed role — reusing a key across two deployments merges their telemetry.

The portable way

Every OpenTelemetry SDK reads these variables, so they work regardless of language or framework:

OTEL_EXPORTER_OTLP_ENDPOINT=http://your-server:4317
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer blgr_..."
# for OTLP/HTTP instead of gRPC:
#   OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
#   OTEL_EXPORTER_OTLP_ENDPOINT=http://your-server:4318

Configuring an exporter in code works equally well — but read the next two sections first, because both failure modes below are silent. An exporter that cannot deliver drops the batch and lets the application run on; nothing appears in Bugler and nothing appears in your logs.

Base address, or full signal path?

Exporters disagree about what an endpoint setting means, and the disagreement is invisible until telemetry goes missing.

Exporter Give it
OTEL_EXPORTER_OTLP_ENDPOINT, any SDK base — the SDK appends /v1/logs, /v1/traces itself
OpenTelemetry .NET, OtlpExporterOptions.Endpoint assigned in code full per-signal URL — the SDK appends nothing
Serilog.Sinks.OpenTelemetry 4.x, options.Endpoint base — the sink derives LogsEndpoint/TracesEndpoint

Getting it wrong lands you on / or on /v1/logs/v1/logs; both answer 404/405 and the batch is gone.

With gRPC the question does not arise — signals are routed by service name, so http://your-server:4317 is correct for every exporter. Prefer gRPC unless something forces HTTP.

One setting shared by two exporters

Applications often export logs through one library and traces through another — a Serilog sink beside an OTel SDK, for instance. If both read the same endpoint setting, a full HTTP path cannot satisfy them: the exporter that appends nothing will send traces and metrics to /v1/logs. Bugler decodes those as a malformed log batch and rejects the whole request with 400, discarding the genuine log records travelling in it. Give each exporter its own setting, or use gRPC.

When nothing arrives

Check the status Bugler returned before suspecting Bugler:

Status Meaning
401 / gRPC UNAUTHENTICATED missing key, or no Service matches it
404 / 405 wrong path — only POST /v1/logs and POST /v1/traces exist
415 Content-Type is not application/x-protobuf
400 body is not a decodable OTLP payload for that signal
503 / gRPC UNAVAILABLE ingest buffer full; the exporter should retry
gRPC UNIMPLEMENTED metrics — Bugler has no metrics receiver yet

Exporters hide these by design, so turn their own diagnostics on: Serilog.Debugging.SelfLog.Enable(…) for a Serilog sink, the equivalent self-diagnostics channel for your SDK. From Bugler's side, running it with Logging__LogLevel__Microsoft.AspNetCore=Information logs every request with its status code, which is usually the fastest way to see what a silent exporter is really sending.

A quick way to prove the server, the key and the network before blaming your application:

curl -i -X POST http://your-server:4318/v1/logs \
  -H "Content-Type: application/x-protobuf" \
  -H "Authorization: Bearer blgr_..." --data-binary ''

An empty body is a valid, empty OTLP request: 200 means the path and the key are good.

Driving the REST API from a script

The REST API on :8080 is the UI's own, and it authenticates with the Session cookie — not with a Service API key, which is for exports only. A script that signs in and then changes something has to send one header of its own:

curl -i -X POST https://bugler.example.com/api/auth/login \
  -H "Content-Type: application/json" \
  -H "Bugler-Request: 1" \
  -d '{"email":"you@example.com","password":"…"}'

Bugler-Request is required on every method that is not a read, its value is never looked at, and without it the answer is 403. It is what keeps a page on another origin from spending your Session: such a page can make your browser send the cookie, but it cannot make it send a header (ADR 0025). Reads need nothing.

Development

Prerequisites: .NET 10 SDK, Bun, Docker.

docker compose up -d          # local PostgreSQL
dotnet run --project src/Bugler.Host
cd frontend && bun dev        # frontend dev server with HMR

The Host listens on :8080 (API/UI), :4317 (OTLP/gRPC), and :4318 (OTLP/HTTP). Database schema migrates automatically at startup. Each process authenticates its exports with its Service API key as a bearer token, e.g. OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer blgr_...".

Tests

Layer Where Run
Unit (backend) tests/Bugler.<Context>.Tests dotnet test
Architecture tests/Bugler.ArchitectureTests (ArchUnitNET) + frontend/.dependency-cruiser.cjs dotnet test / bun run arch
Integration (real PostgreSQL via Testcontainers) tests/Bugler.IntegrationTests dotnet test (needs Docker)
Unit (frontend) frontend/**/*.test.tsx bun test
E2E (Playwright) e2e/tests cd e2e && bun run test (needs docker compose up -d postgres)

Architecture tests enforce the context boundaries described in CONTEXT-MAP.md; a dependency that crosses a context boundary outside its Contracts namespace fails the build.

Status

The whole path is implemented and runs: OTLP ingest of logs and traces, the explore UI, applications, services, API keys and retention, local accounts with per-application grants, and the alerting watch with its mail and Google Chat notifications.

Metrics have no receiver yet, and the version says what it says: Bugler is still on 0.x, because it has not been run in enough places for 1.0 to be an honest number. Until it is, a minor version may move configuration or the database schema underneath you; read the release notes before upgrading.

The current version is whichever is newest under Releases — deliberately not repeated here, since a number written into the repository is made stale by the very commit that writes it.

Contributing and support

Bugler is written and maintained by one person in their own time. Issues are read, and none of the usual promises follow from that: there is no response time, no service level, and no guarantee that a request will be built. Saying so plainly seems better than letting anyone find out by waiting.

Bug reports and questions are welcome as issues. A pull request that fixes a typo, the documentation, or a contained bug can arrive without asking; anything that changes behaviour wants an issue first, so that the shape can be agreed before you spend an afternoon on it. CONTRIBUTING.md has the detail, including what the build enforces about context boundaries and translated strings.

Found a vulnerability? Not in an issue, please — SECURITY.md says where.

Licence

Bugler is licensed under the Apache License 2.0. You may run it, modify it and distribute it, including commercially, provided you keep the copyright notices and state what you changed. It comes with no warranty of any kind. Third-party material Bugler carries — the OpenTelemetry protocol definitions and the IBM Plex typefaces — is listed in NOTICE.

The name

The licence covers the code, not the name. "Bugler" and the Bugler logo are not licensed under Apache 2.0 and remain the author's. A fork is welcome to exist, and must call itself something else — so that nobody installing "Bugler" has to wonder whose it is, and so that its bugs land in its own tracker.

About

Self-hosted OTLP telemetry server for teams running several applications, each deployed per client: logs and traces in, explore UI out, access granted per application, alerting by mail and Google Chat. .NET 10 + React 19 + PostgreSQL, one container.

Topics

Resources

Contributing

Security policy

Stars

2 stars

Watchers

1 watching

Forks

Releases

Used by

Contributors

Languages