# Trying the Official Go SDK

> Source: https://www.ymotongpoo.com/books/tinygo-otel-esp32/15-official-sdk/


## The first setup to try

The most direct way to speak OTLP is to use the official [OpenTelemetry Go](https://opentelemetry.io/docs/languages/go/) as it is. As on a server, you create **instruments** with the API. Instruments are objects that record values, such as counters and gauges. The SDK collects the values, and the exporter sends them to the Collector. In this chapter, I build each of these three layers with [TinyGo](https://tinygo.org/), one at a time, to see where the build stops.

OpenTelemetry Go has three layers. The **API** is the interface for instrumentation, and library authors depend on it. The **SDK** implements the API. It aggregates the values that instruments record, and it attaches resource attributes. Resource attributes tell you which host of which service the data came from. The **exporter** is the component that converts the data that the SDK collected into OTLP and sends it. The exporter that sends metrics over OTLP/HTTP is `otlpmetrichttp`. An application combines all three. A library depends only on the API, and the application chooses the SDK.

For this test, I used TinyGo 0.42.0 and OpenTelemetry Go v1.46.0, with the ESP32-S3 as the target[^target].

## The API and a no-op implementation

First, I built a program that uses only the API and `metric/noop`, the API's no-op implementation. `noop` satisfies the API interfaces and discards every recorded value. You use it to run code that calls the API without the SDK.

This build succeeded, and the binary was 6,400 bytes. The API layer works on TinyGo as it is.

This result matters to people who write libraries with instrumentation. A library that depends only on the OpenTelemetry API builds when you include it in a TinyGo program. In an environment that cannot carry the SDK, the library's instrumentation does nothing through `noop`, but the library itself works. Conversely, if a library depends directly on the SDK or an exporter, that dependency closes the path to using it from TinyGo.

## The SDK and x/sys/unix

Next, I added only the SDK package `sdk/metric`. This build failed with more than 40 instances of the following error.

```
system calls are not supported: target emulates a linux/arm system on xtensa
```

If you trace the dependencies, `sdk/metric` leads through `sdk/resource` to [`golang.org/x/sys/unix`](https://pkg.go.dev/golang.org/x/sys/unix)[^go-mod-why]. `sdk/resource` is the package that automatically collects resource attributes that describe where the data comes from, such as the process name, the OS type, and the host name. To do that, it queries the host OS. For example, `os_unix.go`, which gets the OS information, has the following build constraint and dependency.

```go
//go:build aix || darwin || dragonfly || freebsd || linux || netbsd || openbsd || solaris || zos

package resource

import (
	"fmt"
	"os"

	"golang.org/x/sys/unix"
)
// ...
var defaultUnameProvider unameProvider = unix.Uname
```

As the error message says, TinyGo's Xtensa target is treated as a `linux/arm` system without system calls. It matches `linux` in the build constraint, so this file goes into the build. That brings in system calls such as `unix.Uname`. A microcontroller has no kernel to accept system calls. That is why TinyGo cannot compile these calls.

The part that fails here is not the part that aggregates metrics. At startup, the SDK checks which host it runs on, and its only way to find out is to ask the OS. That is what breaks the build.

## The exporter and tls.X509KeyPair

The exporter `otlpmetrichttp` also failed to build, for a different reason.

```
.../otlpmetrichttp@v1.46.0/internal/envconfig/envconfig.go:143:19: undefined: tls.X509KeyPair
```

`envconfig` is the package that reads the exporter configuration from environment variables such as `OTEL_EXPORTER_OTLP_*`. If an environment variable points to a client certificate file, the package loads that certificate and key and uses them in the TLS configuration. The relevant code is as follows.

```go
		key, err := e.ReadFile(vk)
		// ...
		crt, err := tls.X509KeyPair(cert, key)
		if err != nil {
			global.Error(err, "create tls client key pair")
			return
		}
```

The `crypto/tls` package in TinyGo 0.42.0 has no `X509KeyPair`[^tinygo-tls]. The call sits in a branch that runs only when the environment variable is set. Even so, the compiler must resolve the reference itself at compile time. The build fails even in a setup that sends over plain HTTP without TLS.

## The assumption of a host OS

The failures so far look like this.

| Setup | Result | Where it stopped |
| --- | --- | --- |
| `otel/metric` and `metric/noop` | Success (6,400 bytes) | None |
| `otel/sdk/metric` | Failure | `golang.org/x/sys/unix` through `sdk/resource` |
| `otlpmetrichttp` | Failure | `tls.X509KeyPair` in `envconfig` |

Both failures happen at build time. The binary was not too large for the flash, and the program did not run out of memory at run time. The SDK code assumes that it can learn about the host by asking the OS. The exporter code assumes a complete `crypto/tls` that can handle certificates. On servers and PCs, both assumptions always hold, so the code does not need to be aware of them.

**The SDK does not fit on a microcontroller because of dependencies that assume a host OS, not because of the amount of memory.** As Chapter 2 showed, OpenTelemetry was not designed for embedded use. These two build errors are the concrete form of that design assumption.

Once you know the causes of the failures, you also know what you need to write yourself on the device side. The SDK collected resource attributes by asking the OS. In their place, the code holds values for what the device knows about itself: the service name, the version, and the device ID and model. In this book's implementation, the device does not hold attributes that depend on the environment, such as `deployment.environment.name`. The Collector adds them[^collector]. The device does not read the exporter configuration from environment variables. The build embeds it instead. The device does not carry TLS, and it sends to the Collector on the same network over plain HTTP.

What remains is the part that holds the values that instruments record, builds them into OTLP messages, and sends them over HTTP. The SDK did this work. In this book's implementation, I rewrote it in a small form. It is a type that only registers instruments and holds their values, with no aggregation mechanism. In the next chapter, let's try the official component that turns those messages into bytes: the protobuf runtime.

[^target]: Strictly speaking, I ran the builds in this chapter with the `xiao-esp32s3` target. Both `xiao-esp32s3` and `esp32s3-generic`, which this book uses on the real device, inherit from TinyGo's `esp32s3`. The only difference is the build tags.
[^go-mod-why]: I confirmed the dependency path with `go mod why`.
[^tinygo-tls]: In TinyGo 0.42.0, `src/crypto/tls/tls.go` contains `LoadX509KeyPair`, which reads from files, as a function that only returns an "unimplemented" error. `X509KeyPair`, which takes PEM data in memory, is not declared.
[^collector]: Chapter 11 covers adding attributes in the Collector.

