Skip to content

Commit

Permalink
feat(otel): metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
lukasmalkmus committed Dec 8, 2023
1 parent 9d7d26d commit 13d7dd9
Show file tree
Hide file tree
Showing 17 changed files with 492 additions and 148 deletions.
16 changes: 12 additions & 4 deletions .github/workflows/test_examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ jobs:
# - ingesthackernews
- logrus
- otelinstrument
- oteltraces
- otelmetric
- oteltrace
- query
- querylegacy
# HINT(lukasmalkmus): This test would require Go 1.21 (but uses Go
Expand Down Expand Up @@ -65,10 +66,13 @@ jobs:
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 3 )'
- example: otelinstrument
verify: |
axiom dataset info _traces -f=json | jq -e 'any( .numEvents ; . >= 1 )'
- example: oteltraces
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 1 )'
- example: otelmetric
verify: |
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . > 3 )'
- example: oteltrace
verify: |
axiom dataset info _traces -f=json | jq -e 'any( .numEvents ; . >= 1 )'
axiom dataset info $AXIOM_DATASET -f=json | jq -e 'any( .numEvents ; . == 1 )'
- example: query
setup: |
echo '[{"mood":"hyped","msg":"This is awesome!"}]' >> logs.json
Expand Down Expand Up @@ -107,6 +111,10 @@ jobs:
run: ${{ matrix.setup }}
- name: Run example
run: go run ./examples/${{ matrix.example }}/main.go
timeout-minutes: 5
# We have some long running examples so cancel the step after a while.
# We still validate the example output in the next step.
continue-on-error: true
- name: Verify example
if: matrix.verify
run: ${{ matrix.verify }}
Expand Down
112 changes: 112 additions & 0 deletions axiom/otel/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package otel

import (
"time"

"github.com/axiomhq/axiom-go/internal/config"
)

const (
defaultMetricAPIEndpoint = "/v1/metrics"
defaultTraceAPIEndpoint = "/v1/traces"
)

// Config is the configuration for OpenTelemetry components initialized by this
// helper package. This type is exported for convenience but an [Option] is
// naturally applied by one or more "Set"-prefixed functions.
type Config struct {
config.Config

// APIEndpoint is the endpoint to use for an exporter.
APIEndpoint string
// Timeout is the timeout for an exporters underlying [http.Client].
Timeout time.Duration
// NoEnv disables the use of "AXIOM_*" environment variables.
NoEnv bool
}

func defaultMetricConfig() Config {
return Config{
Config: config.Default(),
APIEndpoint: defaultMetricAPIEndpoint,
}
}

func defaultTraceConfig() Config {
return Config{
Config: config.Default(),
APIEndpoint: defaultTraceAPIEndpoint,
}
}

// An Option modifies the behaviour of OpenTelemetry exporters. Nonetheless,
// the official "OTEL_*" environment variables are preferred over the options or
// "AXIOM_*" environment variables.
type Option func(c *Config) error

// SetURL sets the base URL used by the client.
//
// Can also be specified using the "AXIOM_URL" environment variable.
func SetURL(baseURL string) Option {
return func(c *Config) error { return c.Options(config.SetURL(baseURL)) }
}

// SetToken specifies the authentication token used by the client.
//
// Can also be specified using the "AXIOM_TOKEN" environment variable.
func SetToken(token string) Option {
return func(c *Config) error { return c.Options(config.SetToken(token)) }
}

// SetOrganizationID specifies the organization ID used by the client.
//
// Can also be specified using the "AXIOM_ORG_ID" environment variable.
func SetOrganizationID(organizationID string) Option {
return func(c *Config) error { return c.Options(config.SetOrganizationID(organizationID)) }
}

// SetAPIEndpoint specifies the api endpoint used by the client.
func SetAPIEndpoint(path string) Option {
return func(c *Config) error {
c.APIEndpoint = path
return nil
}
}

// SetTimeout specifies the http timeout used by the client.
func SetTimeout(timeout time.Duration) Option {
return func(c *Config) error {
c.Timeout = timeout
return nil
}
}

// SetNoEnv prevents the client from deriving its configuration from the
// environment (by auto reading "AXIOM_*" environment variables).
func SetNoEnv() Option {
return func(c *Config) error {
c.NoEnv = true
return nil
}
}

func populateAndValidateConfig(base *Config, options ...Option) error {
// Apply supplied options.
for _, option := range options {
if option == nil {
continue
} else if err := option(base); err != nil {
return err
}
}

// Make sure to populate remaining fields from the environment, if not
// explicitly disabled.
if !base.NoEnv {
if err := base.IncorporateEnvironment(); err != nil {
return err
}
}

return base.Validate()
}
30 changes: 16 additions & 14 deletions axiom/otel/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,25 @@
//
// import "github.com/axiomhq/axiom-go/axiom/otel"
//
// Different levels of helpers are available, from just setting up tracing to
// getting access to lower level components to costumize tracing or integrate
// with existing OpenTelemetry setups:
//
// - [InitTracing]: Initializes OpenTelemetry and sets the global tracer
// prodiver so the official OpenTelemetry Go SDK can be used to get a tracer
// and instrument code. Sane defaults for the tracer provider are applied.
// - [TracerProvider]: Configures and returns a new OpenTelemetry tracer
// provider but does not set it as the global tracer provider.
// - [TraceExporter]: Configures and returns a new OpenTelemetry trace
// exporter. This sets up the exporter that sends traces to Axiom but allows
// for a more advanced setup of the tracer provider.
// Different levels of helpers are available, from just setting up
// instrumentation to getting access to lower level components to costumize
// instrumentation or integrate with existing OpenTelemetry setups:
//
// - [InitMetrics]/[InitTracing]: Initializes OpenTelemetry and sets the
// global meter/tracer prodiver so the official OpenTelemetry Go SDK can be
// used to get a meter/tracer and instrument code. Sane defaults for the
// providers are applied.
// - [MeterProvider]/[TracerProvider]: Configures and returns a new
// OpenTelemetry meter/tracer provider but does not set it as the global
// meter/tracer provider.
// - [MetricExporter]/[TraceExporter]: Configures and returns a new
// OpenTelemetry metric/trace exporter. This sets up the exporter that sends
// metrics/traces to Axiom but allows for a more advanced setup of the
// meter/tracer provider.
//
// If you wish for traces to propagate beyond the current process, you need to
// set the global propagator to the OpenTelemetry trace context propagator. This
// can be done
// by calling:
// can be done by calling:
//
// import (
// "go.opentelemetry.io/otel"
Expand Down
108 changes: 108 additions & 0 deletions axiom/otel/metric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package otel

import (
"context"
"fmt"
"time"

"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
"go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

// MetricExporter configures and returns a new exporter for OpenTelemetry spans.
func MetricExporter(ctx context.Context, dataset string, options ...Option) (metric.Exporter, error) {
config := defaultMetricConfig()

if err := populateAndValidateConfig(&config, options...); err != nil {
return nil, err
}

u, err := config.BaseURL().Parse(config.APIEndpoint)
if err != nil {
return nil, fmt.Errorf("parse exporter url: %w", err)
}

opts := []otlpmetrichttp.Option{
otlpmetrichttp.WithEndpoint(u.Host),
}
if u.Path != "" {
opts = append(opts, otlpmetrichttp.WithURLPath(u.Path))
}
if u.Scheme == "http" {
opts = append(opts, otlpmetrichttp.WithInsecure())
}
if config.Timeout > 0 {
opts = append(opts, otlpmetrichttp.WithTimeout(config.Timeout))
}

headers := make(map[string]string)
if config.Token() != "" {
headers["Authorization"] = "Bearer " + config.Token()
}
if config.OrganizationID() != "" {
headers["X-Axiom-Org-Id"] = config.OrganizationID()
}
if dataset != "" {
headers["X-Axiom-Dataset"] = dataset
}
if len(headers) > 0 {
opts = append(opts, otlpmetrichttp.WithHeaders(headers))
}

return otlpmetrichttp.New(ctx, opts...)
}

// MeterProvider configures and returns a new OpenTelemetry meter provider.
func MeterProvider(ctx context.Context, dataset, serviceName, serviceVersion string, options ...Option) (*metric.MeterProvider, error) {
exporter, err := MetricExporter(ctx, dataset, options...)
if err != nil {
return nil, err
}

rs, err := resource.Merge(resource.Default(), resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String(serviceVersion),
UserAgentAttribute(),
))
if err != nil {
return nil, err
}

opts := []metric.Option{
metric.WithReader(metric.NewPeriodicReader(
exporter,
metric.WithInterval(time.Second*5), // FIXME(lukasmalkmus): Just for testing!
metric.WithTimeout(time.Second*5), // FIXME(lukasmalkmus): Just for testing!
)),
metric.WithResource(rs),
}

return metric.NewMeterProvider(opts...), nil
}

// InitMetrics initializes OpenTelemetry metrics with the given service name,
// version and options. If initialization succeeds, the returned cleanup
// function must be called to shut down the meter provider and flush any
// remaining datapoints. The error returned by the cleanup function must be
// checked, as well.
func InitMetrics(ctx context.Context, dataset, serviceName, serviceVersion string, options ...Option) (func() error, error) {
meterProvider, err := MeterProvider(ctx, dataset, serviceName, serviceVersion, options...)
if err != nil {
return nil, err
}

otel.SetMeterProvider(meterProvider)

closeFunc := func() error {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()

return meterProvider.Shutdown(ctx)
}

return closeFunc, nil
}
53 changes: 53 additions & 0 deletions axiom/otel/metric_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
//go:build integration

package otel_test

import (
"context"
"fmt"
"os"
"testing"

"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"

"github.com/axiomhq/axiom-go/axiom"
axiotel "github.com/axiomhq/axiom-go/axiom/otel"
)

func TestMetricsIntegration(t *testing.T) {
ctx := context.Background()

datasetSuffix := os.Getenv("AXIOM_DATASET_SUFFIX")
if datasetSuffix == "" {
datasetSuffix = "local"
}
dataset := fmt.Sprintf("test-axiom-go-otel-metric-%s", datasetSuffix)

client, err := axiom.NewClient()
require.NoError(t, err)

_, err = client.Datasets.Create(ctx, axiom.DatasetCreateRequest{
Name: dataset,
Description: "This is a test dataset for otel metric integration tests.",
})
require.NoError(t, err)

t.Cleanup(func() {
err = client.Datasets.Delete(ctx, dataset)
require.NoError(t, err)
})

stop, err := axiotel.InitMetrics(ctx, dataset, "axiom-go-otel-test-metric", "v1.0.0")
require.NoError(t, err)
require.NotNil(t, stop)

t.Cleanup(func() { require.NoError(t, stop()) })

meter := otel.Meter("main")

counter, err := meter.Int64Counter("test")
require.NoError(t, err)

counter.Add(ctx, 1)
}
Loading

0 comments on commit 13d7dd9

Please sign in to comment.