Skip to content
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
610922f
chore(changelog): backmerge main into develop [skip ci]
lerian-studio May 16, 2026
b847122
build: upgrade Go to 1.26.3 and update project dependencies and tooling
fredcamaral May 19, 2026
f43c51d
feat(middleware): skip /readyz by default and add WithExcludedRoutes
brunognovaes May 19, 2026
5f3e048
feat(middleware): skip probe and scrape paths by default; add WithExc…
brunognovaes May 19, 2026
bfa4277
Merge pull request #10 from LerianStudio/fix/remove-readyz-logging
brunognovaes May 19, 2026
43e5780
docs: remove internal Gandalf call instructions (#12)
gandalf-at-lerian May 21, 2026
4f4da53
chore(changelog): backmerge main into develop [skip ci]
lerian-studio May 21, 2026
972c752
feat(middleware): record http.server.request.duration in WithTelemetry
gandalf-at-lerian May 26, 2026
b57e0cf
fix(middleware): record effective status for handler errors
gandalf-at-lerian May 27, 2026
6e138f1
fix(middleware): replace http.NewRequest with httptest.NewRequest in …
gauchito91 Jun 1, 2026
31e68d6
fix(middleware): normalize http.request.method to OTel known set
gauchito91 Jun 1, 2026
cd52e38
fix(middleware): make error.type status-driven for consistent aggrega…
gauchito91 Jun 1, 2026
6fac9d7
fix(middleware): align duration histogram buckets with OTel advisory
gauchito91 Jun 1, 2026
8a4974e
fix(middleware): omit http.route for unmatched 404 requests
gauchito91 Jun 1, 2026
487013c
docs(middleware): clarify why MetricsFactory gates duration recording
gauchito91 Jun 1, 2026
2b76cd5
fix(middleware): truncate user_agent.original span attribute to 256 b…
gauchito91 Jun 1, 2026
ed50bb2
Merge branch 'develop' into feat/http-server-request-metrics
gauchito91 Jun 1, 2026
52675b5
fix(middleware): extract span attribute helper to satisfy gocyclo
gauchito91 Jun 1, 2026
8cc45a8
Merge pull request #13 from LerianStudio/feat/http-server-request-met…
gauchito91 Jun 1, 2026
035c84b
chore(deps): bump the go-dependencies group with 12 updates
dependabot[bot] Jun 2, 2026
21683b8
feat: add tenant.id as shared observability attribute
gandalf-at-lerian Jun 2, 2026
cfa1550
refactor(observability): redesign tenant.id propagation
gandalf-at-lerian Jun 2, 2026
98523c6
refactor: move tenant constants to constants package
gandalf-at-lerian Jun 2, 2026
ac3c6a4
Merge pull request #14 from LerianStudio/feat/tenant-id-observability…
qnen Jun 2, 2026
57936c1
feat(middleware): add tenant.id to http.server.request.duration metric
gauchito91 Jun 3, 2026
b532a30
fix(middleware): re-sanitize tenant.id read from AttrBag (CodeRabbit)
gauchito91 Jun 3, 2026
754bbff
Merge pull request #16 from LerianStudio/feat/http-metric-tenant-id-d…
qnen Jun 3, 2026
94239a5
fix(tracing): infer insecure exporter for scheme-less OTLP endpoint
qnen Jun 5, 2026
5a733f8
fix(tracing): log os.Setenv failures in OTEL env normalization
qnen Jun 5, 2026
3b74c9f
Merge pull request #17 from LerianStudio/fix/otel-insecure-endpoint
qnen Jun 5, 2026
ce45bfa
fix(tracing): replace deprecated attr.Value.Emit with Value.String
qnen Jun 5, 2026
d2da99a
Merge pull request #15 from LerianStudio/dependabot/go_modules/develo…
qnen Jun 5, 2026
deac21d
feat(observability): read tenant.id from OTel baggage for spans and logs
gandalf-at-lerian Jun 8, 2026
d7a8801
fix(tracing): sanitize tenant.id from baggage before seeding spans
gandalf-at-lerian Jun 9, 2026
09b1074
fix(lint): add blank line before if in sanitizeTenantFromBaggage
gandalf-at-lerian Jun 9, 2026
0f7ced0
Merge pull request #20 from LerianStudio/feat/read-tenant-id-from-ote…
qnen Jun 9, 2026
4f51192
fix(middleware): propagate tenant.id to AttrBag in SetHandlerSpanAttr…
gandalf-at-lerian Jun 10, 2026
518140f
refactor(middleware): use AttrKeyTenantID constant in all span helpers
gandalf-at-lerian Jun 10, 2026
a044f5c
refactor(constants): promote context.id to shared AttrKeyContextID co…
gandalf-at-lerian Jun 10, 2026
f90a31e
Merge pull request #21 from LerianStudio/fix/set-handler-span-attribu…
gauchito91 Jun 10, 2026
7b11b2b
fix(middleware): include baggage-propagated tenant.id in http.server.…
gandalf-at-lerian Jun 10, 2026
f0ae218
Merge pull request #22 from LerianStudio/fix/metric-tenant-id-attribute
gauchito91 Jun 10, 2026
8962473
fix(logging): add structured http_status_code, http_method, http_path…
gandalf-at-lerian Jun 11, 2026
2ffbd8a
fix(logging): add http_latency_ms field to WithHTTPLogging access log
gandalf-at-lerian Jun 11, 2026
2428e19
Merge pull request #23 from LerianStudio/fix/http-logging-structured-…
qnen Jun 11, 2026
44797f6
fix(tracing): persist trimmed endpoint and treat null/nil baggage as …
gandalf-at-lerian Jun 16, 2026
307fdac
Merge pull request #25 from LerianStudio/fix/coderabbit-otel-normaliz…
qnen Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/go-combined-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ jobs:
with:
runner_type: "blacksmith-4vcpu-ubuntu-2404"
app_name_prefix: "lib-observability"
go_version: "1.25.10"
golangci_lint_version: "v2.11.2"
go_version: "1.26.3"
golangci_lint_version: "v2.12.2"
golangci_lint_args: "--timeout=5m"
coverage_threshold: 80
fail_on_coverage_threshold: true
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
## What this repo is

`github.com/LerianStudio/lib-observability` — standalone Go library for observability/telemetry,
extracted from `lib-commons` (`../lib-commons`). Go 1.25.10.
extracted from `lib-commons` (`../lib-commons`). Go 1.26.3.

## Architecture decisions (non-negotiable)

Expand Down
65 changes: 51 additions & 14 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ endef
# ------------------------------------------------------

# Integration test filter
# RUN: specific test name pattern (e.g., TestIntegration_FeatureName)
# PKG: specific package to test (e.g., ./...)
# Usage: make test-integration RUN=TestIntegration_FeatureName
# make test-integration PKG=./...
RUN ?=
PKG ?=

Expand All @@ -32,6 +36,11 @@ else
endif

# Low-resource mode for limited machines (sets -p=1 -parallel=1, disables -race)
# Usage: make test LOW_RESOURCE=1
# make test-unit LOW_RESOURCE=1
# make test-integration LOW_RESOURCE=1
# make coverage-unit LOW_RESOURCE=1
# make coverage-integration LOW_RESOURCE=1
LOW_RESOURCE ?= 0

# Computed flags for low-resource mode
Expand All @@ -45,9 +54,12 @@ else
LOW_RES_RACE_FLAG := -race
endif

# macOS ld64 workaround
# macOS ld64 workaround: newer ld emits noisy LC_DYSYMTAB warnings when linking test binaries with -race.
# If available, prefer Apple's classic linker to silence them.
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Darwin)
# Prefer classic mode to suppress LC_DYSYMTAB warnings on macOS.
# Set DISABLE_OSX_LINKER_WORKAROUND=1 to disable this behavior.
ifneq ($(DISABLE_OSX_LINKER_WORKAROUND),1)
GO_TEST_LDFLAGS := -ldflags="-linkmode=external -extldflags=-ld_classic"
else
Expand All @@ -61,18 +73,22 @@ endif
# Test tooling configuration
# ------------------------------------------------------

GOTESTSUM_VERSION ?= v1.12.0
GOSEC_VERSION ?= v2.22.4
GOLANGCI_LINT_VERSION ?= v2.1.6
# Pinned tool versions for reproducibility (update as needed)
GOTESTSUM_VERSION ?= v1.13.0
GOSEC_VERSION ?= v2.26.1
GOLANGCI_LINT_VERSION ?= v2.12.2

TEST_REPORTS_DIR ?= ./reports
GOTESTSUM = $(shell command -v gotestsum 2>/dev/null)
GOSEC = $(shell command -v gosec 2>/dev/null || \
GOBIN="$$(go env GOBIN)"; \
if [ -n "$$GOBIN" ]; then \
printf "%s/gosec" "$$GOBIN"; \
GOSEC = $(shell if command -v gosec >/dev/null 2>&1; then \
command -v gosec; \
else \
printf "%s/bin/gosec" "$$(go env GOPATH)"; \
GOBIN="$$(go env GOBIN)"; \
if [ -n "$$GOBIN" ]; then \
printf "%s/gosec" "$$GOBIN"; \
else \
printf "%s/bin/gosec" "$$(go env GOPATH)"; \
fi; \
fi)
RETRY_ON_FAIL ?= 0

Expand Down Expand Up @@ -216,7 +232,7 @@ test-unit:
$(call print_title,Running Go unit tests)
$(call check_command,go,"Install Go from https://golang.org/doc/install")
@set -e; mkdir -p $(TEST_REPORTS_DIR); \
pkgs=$$(go list ./... | grep -v '/tests'); \
pkgs=$$(go list -tags=unit ./...); \
if [ -z "$$pkgs" ]; then \
echo "No unit test packages found"; \
else \
Expand Down Expand Up @@ -318,9 +334,9 @@ coverage-unit:
@set -e; mkdir -p $(TEST_REPORTS_DIR); \
if [ -n "$(PKG)" ]; then \
echo "Using specified package: $(PKG)"; \
pkgs=$$(go list $(PKG) 2>/dev/null | grep -v '/tests' | tr '\n' ' '); \
pkgs=$$(go list -tags=unit $(PKG) 2>/dev/null | tr '\n' ' '); \
else \
pkgs=$$(go list ./... | grep -v '/tests'); \
pkgs=$$(go list -tags=unit ./...); \
fi; \
if [ -z "$$pkgs" ]; then \
echo "No unit test packages found"; \
Expand Down Expand Up @@ -440,8 +456,29 @@ coverage:
lint:
$(call print_title,Running linters on all packages (read-only))
$(call check_command,golangci-lint,"go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)")
@golangci-lint run ./...
@echo "$(GREEN)$(BOLD)[ok]$(NC) Lint checks passed successfully$(GREEN) ✔️$(NC)"
@out=$$(golangci-lint run ./... 2>&1); \
out_err=$$?; \
if command -v perfsprint >/dev/null 2>&1; then \
perf_out=$$(perfsprint ./... 2>&1); \
perf_err=$$?; \
else \
perf_out=""; \
perf_err=0; \
echo "Note: perfsprint not installed, skipping performance checks (go install github.com/catenacyber/perfsprint@latest)"; \
fi; \
echo "$$out"; \
if [ -n "$$perf_out" ]; then echo "$$perf_out"; fi; \
if [ $$out_err -ne 0 ]; then \
printf "\n%s\n" "$(BOLD)$(RED)An error has occurred during the lint process:$(NC)"; \
printf "%s\n" "$$out"; \
exit 1; \
fi; \
if [ $$perf_err -ne 0 ]; then \
printf "\n%s\n" "$(BOLD)$(RED)An error has occurred during the performance check:$(NC)"; \
printf "%s\n" "$$perf_out"; \
exit 1; \
fi
@echo "$(GREEN)$(BOLD)[ok]$(NC) Lint and performance checks passed successfully$(GREEN) ✔️$(NC)"

.PHONY: lint-fix
lint-fix:
Expand Down
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ A configurable `Redactor` with rule-based field processing supporting mask, hash
- **Redaction-first** — sensitive fields are masked in spans, logs, and attributes by default
- **Interface-driven** — `Logger`, `MetricsFactory`, `ErrorReporter`, and `DLQMetrics` are all interface-bound for testability

## Tenant ID propagation

The HTTP and gRPC middleware automatically read a tenant identifier from the request and propagate it through telemetry as the `tenant.id` attribute / log field.

### Wire protocol

- **HTTP:** canonical header `X-Tenant-Id`. No aliases.
- **gRPC:** canonical metadata key `tenant-id`. No aliases.

Values are normalized (trimmed, control chars stripped) and dropped silently when empty or longer than 128 bytes to bound telemetry cardinality.

### Where it shows up

| Signal | How it gets there | Action required by caller |
|---|---|---|
| Logs | `WithHTTPLogging` / `WithGrpcLogging` add `tenant.id` as a structured field. | None |
| Traces | The `AttrBagSpanProcessor` (registered by default in `tracing.NewTelemetry`) copies the request attribute bag onto every span at start. | None |
| `http.server.request.duration` (built-in metric) | `WithTelemetry` adds `tenant.id` to the histogram when present in the request bag. Label is omitted when no tenant was supplied so non-tenant traffic does not split the time series. | None |
| Custom application metrics | Not automatic. Metric labels are a high-impact cardinality decision left to the caller. | Use `middleware.RequestAttributes(ctx)` to opt in per metric. |

Example for custom metrics:

```go
import "github.com/LerianStudio/lib-observability/middleware"

counter, _ := factory.Counter("orders.created")
_ = counter.
WithAttributes(middleware.RequestAttributes(ctx)...).
Add(ctx, 1)
```

### Trust boundary

The header is **client-controlled**. The middleware treats it as an observability hint, not an authenticated identifier:

- Run authentication (JWT, mTLS, etc.) **before** these middlewares apply effects you care about.
- If your auth layer resolves the real tenant from a signed credential, call `observability.ContextWithSpanAttributes(ctx, attribute.String("tenant.id", real))` from the handler. The attribute bag is deduplicated by key with last-wins semantics, so the override surfaces in subsequent logs/traces and replaces the header-supplied value.

## Relationship to lib-commons

This library was extracted from `lib-commons` to decouple observability infrastructure from service primitives and data connectors. Services that previously imported `lib-commons` for telemetry can migrate to `lib-observability` for a lighter dependency graph. `lib-commons` will depend on `lib-observability` for its own instrumentation needs (database spans, streaming metrics, middleware telemetry).
20 changes: 20 additions & 0 deletions constants/tenant.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package constants

const (
// HeaderTenantID is the canonical HTTP header carrying the tenant
// identifier across Lerian services.
HeaderTenantID = "X-Tenant-Id"

// MetadataTenantID is the canonical gRPC metadata key carrying the
// tenant identifier. gRPC metadata keys are lowercase by spec.
MetadataTenantID = "tenant-id"

// AttrKeyTenantID is the OpenTelemetry attribute / log field key used
// everywhere tenant.id is emitted (logs, traces, metrics).
AttrKeyTenantID = "tenant.id"

// MaxTenantIDLen caps tenant IDs extracted from request headers to keep
// telemetry cardinality bounded. Values exceeding the cap are dropped
// silently.
MaxTenantIDLen = 128
)
54 changes: 27 additions & 27 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,58 +1,58 @@
module github.com/LerianStudio/lib-observability

go 1.25.10
go 1.26.3

require (
github.com/gofiber/fiber/v2 v2.52.13
github.com/google/uuid v1.6.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.11.1
go.opentelemetry.io/contrib/bridges/otelzap v0.18.0
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0
go.opentelemetry.io/otel/log v0.19.0
go.opentelemetry.io/otel/metric v1.43.0
go.opentelemetry.io/otel/sdk v1.43.0
go.opentelemetry.io/otel/sdk/log v0.19.0
go.opentelemetry.io/otel/sdk/metric v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
go.opentelemetry.io/contrib/bridges/otelzap v0.19.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/metric v1.44.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/log v0.20.0
go.opentelemetry.io/otel/sdk/metric v1.44.0
go.opentelemetry.io/otel/trace v1.44.0
go.uber.org/mock v0.6.0
go.uber.org/zap v1.28.0
google.golang.org/grpc v1.81.0
google.golang.org/grpc v1.81.1
)

require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/klauspost/compress v1.18.5 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.21 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-runewidth v0.0.23 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect
github.com/tklauser/go-sysconf v0.4.0 // indirect
github.com/tklauser/numcpus v0.12.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
github.com/valyala/fasthttp v1.71.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sys v0.43.0 // indirect
golang.org/x/text v0.36.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
Loading