Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ go.work
# OS specific files
.DS_Store
Thumbs.db
/docs/
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-06-20

### Added

- `integrations/otelmetric`: opt-in OpenTelemetry metrics exporter. Implements `sdkmetric.Exporter` and bridges counters, gauges and histograms into the LogTide pipeline as log entries, inheriting the client's service name, environment, tags and resource attributes (closes #2)
- exemplar support: when a data point carries an exemplar with trace context, the entry is linked to that `trace_id`/`span_id` and the full exemplar list is recorded under `metadata.metric.exemplars`

## [0.9.4] - 2026-06-11

### Added
Expand Down
1 change: 1 addition & 0 deletions CONFORMANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,4 @@ why a scenario does not apply. TODO entries are tracked work.
| C26 | log/trace correlation: active span ids on entries | ✅ | `tracing_test.go` (OTel context extraction) |
| C27 | middleware error capture rethrows after logging | ✅ | `integrations/nethttp/nethttp_test.go` |
| C28 | logging-bridge level mapping and scope context | ✅ | `integrations/logtideslog/logtideslog_test.go` |
| C29 | OTLP metric export (counters/gauges/histograms) with resource attributes | ✅ | `integrations/otelmetric/otelmetric_test.go` (OTel-native path) |
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
- **Automatic batching** — configurable batch size and flush interval
- **Retry with backoff** — exponential backoff with jitter
- **Circuit breaker** — prevents cascading failures
- **OpenTelemetry** — trace/span IDs extracted automatically; span exporter included
- **OpenTelemetry** — trace/span IDs extracted automatically; span and metric exporters included
- **net/http middleware** — per-request scope isolation out of the box
- **Thread-safe** — safe for concurrent use

Expand Down Expand Up @@ -224,6 +224,43 @@ tp := sdktrace.NewTracerProvider(
)
```

### Metric exporter

Export OpenTelemetry metrics (counters, gauges, histograms) to LogTide. Each
data point becomes a log entry carrying the same service name, environment, tags
and resource attributes as your logs and traces. It is opt-in — register it only
if you need metrics.

```go
import (
sdkmetric "go.opentelemetry.io/otel/sdk/metric"

"github.com/logtide-dev/logtide-sdk-go/integrations/otelmetric"
)

integration := otelmetric.New()

flush := logtide.Init(logtide.ClientOptions{
DSN: "https://lp_abc@api.logtide.dev",
Service: "my-service",
Integrations: func(defaults []logtide.Integration) []logtide.Integration {
return append(defaults, integration)
},
})
defer flush()

mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(sdkmetric.NewPeriodicReader(integration.Exporter())),
)
otel.SetMeterProvider(mp)
```

When metric exemplars are enabled (an exemplar filter is configured and a sampled
span is active during measurement), each linked data point's entry inherits the
exemplar's `trace_id`/`span_id`, and the full exemplar list is recorded under
`metadata.metric.exemplars` — correlating metrics with the traces that produced
them.

---

## Flush & shutdown
Expand Down Expand Up @@ -278,6 +315,7 @@ client, _ := logtide.NewClient(logtide.ClientOptions{
| [examples/echo](./examples/echo) | Echo framework integration |
| [examples/stdlib](./examples/stdlib) | Standard library net/http |
| [examples/otel](./examples/otel) | OpenTelemetry distributed tracing |
| [examples/otelmetric](./examples/otelmetric) | OpenTelemetry metrics export |

---

Expand Down
88 changes: 88 additions & 0 deletions examples/otelmetric/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Command otelmetric demonstrates exporting OpenTelemetry metrics through
// LogTide. Counters, gauges and histograms recorded against the MeterProvider
// are converted to LogTide log entries carrying the same service name,
// environment, tags and resource attributes as your logs and traces.
package main

import (
"context"
"log"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"

logtide "github.com/logtide-dev/logtide-sdk-go"
"github.com/logtide-dev/logtide-sdk-go/integrations/otelmetric"
)

func main() {
// Create the metric integration and register it with LogTide.
integration := otelmetric.New()

flush := logtide.Init(logtide.ClientOptions{
DSN: "https://lp_your_api_key_here@api.logtide.dev",
Service: "otelmetric-example",
Integrations: func(defaults []logtide.Integration) []logtide.Integration {
return append(defaults, integration)
},
})
defer flush()

// Wire the exporter into a MeterProvider. A PeriodicReader collects metrics
// on an interval and hands them to the exporter.
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(
sdkmetric.NewPeriodicReader(
integration.Exporter(),
sdkmetric.WithInterval(2*time.Second),
),
),
)
defer func() {
if err := mp.Shutdown(context.Background()); err != nil {
log.Printf("Error shutting down meter provider: %v", err)
}
}()
otel.SetMeterProvider(mp)

meter := mp.Meter("logtide-metric-example")

// A counter.
requests, err := meter.Int64Counter(
"http.requests",
metric.WithDescription("Number of handled HTTP requests"),
metric.WithUnit("1"),
)
if err != nil {
log.Fatalf("create counter: %v", err)
}

// A histogram.
latency, err := meter.Float64Histogram(
"http.latency",
metric.WithDescription("Request latency"),
metric.WithUnit("ms"),
)
if err != nil {
log.Fatalf("create histogram: %v", err)
}

ctx := context.Background()

// Record some measurements.
for i := 0; i < 5; i++ {
requests.Add(ctx, 1)
latency.Record(ctx, float64(40+i*7))
time.Sleep(200 * time.Millisecond)
}

// Force a final collection before exit so the metrics are flushed.
if err := mp.ForceFlush(ctx); err != nil {
log.Printf("force flush: %v", err)
}

log.Println("OpenTelemetry metrics example completed!")
log.Println("Counter and histogram values were exported to LogTide as log entries.")
}
5 changes: 3 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ module github.com/logtide-dev/logtide-sdk-go
go 1.23.0

require (
go.opentelemetry.io/otel v1.38.0
go.opentelemetry.io/otel/metric v1.38.0
go.opentelemetry.io/otel/sdk v1.38.0
go.opentelemetry.io/otel/sdk/metric v1.38.0
go.opentelemetry.io/otel/trace v1.38.0
)

Expand All @@ -12,7 +15,5 @@ require (
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/uuid v1.6.0 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
golang.org/x/sys v0.35.0 // indirect
)
Loading
Loading