First published: Last updated:

Assumptions and Constraints

Originally published in Japanese at https://zenn.dev/ymotongpoo/books/tinygo-otel-esp32/viewer/05-constraints.

Before the main part, this chapter explains each of the constraints that I built the implementation under. Many of the decisions in later chapters come from one of the constraints listed here.

A specification that does not assume embedded devices

The first assumption is that the OpenTelemetry specification was not designed with embedded devices in mind. In 2024, someone opened issue #2160 to propose a working group. The group would build an implementation for embedded devices that run C, such as the ESP32. Ted Young (tedsuo)1, a co-founder of OpenTelemetry, commented on it as follows.

In general, the OTel spec is not designed for the limited resources present in embedded systems. I don’t think that the client design represents the right trade-offs best suited for those environments.

The same comment also says that OTLP is too heavy for the network. A 128-bit trace ID is too large for many of the messaging protocols that embedded devices use, and the full W3C tracing headers fit even less. The comment then concludes that no part of OpenTelemetry is likely to be a good starting point for a general-purpose telemetry implementation for embedded devices.

What this comment rules out is a general-purpose implementation that works for embedded devices in general. This book has a much narrower scope: one microcontroller that speaks TCP/IP over Wi-Fi, and an OpenTelemetry Collector on the same LAN. Within this scope, OTLP messages are small enough to send over Wi-Fi, and a 128-bit trace ID is not a burden. On the other hand, the point that the client design does not suit microcontrollers applies as it is. Chapter 4 examines what that means in practice.

Hardware constraints

The M5Stack CoreS3 that this book uses is a board with Espressif’s ESP32-S3. The ESP32-S3 is a dual-core microcontroller that runs at up to 240 MHz and has built-in Wi-Fi2. The board’s specification lists 16 MB of flash memory and 8 MB of PSRAM. Flash memory is the non-volatile storage that holds the program. PSRAM is additional RAM attached outside the chip.

However, TinyGo 0.42.0 can use only the SRAM built into the chip. The linker script allocates 416 KB of RAM (DRAM) for data, which is 425,984 bytes. Of that, the heap limit was 298,287 bytes. The 8 MB of PSRAM is not yet usable from TinyGo3.

The program runs with much less memory than the board’s specification suggests. When I measured it, though, the working set for this use fit in the built-in SRAM. The problem was less the amount of memory and more the memory that each export allocates and then discards. Chapter 9 covers this.

The 16 MB of flash memory is enough as an upper limit on program size. Still, the binary size changes by hundreds of KB depending on which standard packages you use. Chapter 7 looks at that difference.

TinyGo constraints

The standard library and runtime of TinyGo leave out some parts to fit small environments. Three of them caused real problems in this book.

The first is the reflect package. TinyGo’s reflect has methods that are declared but panic when you call them. (reflect.Type).MethodByName is one of them. The program builds and links, but it crashes on the real device at the moment the method is called. The official Go protobuf runtime calls this method the first time it encodes a message, so you cannot use the runtime as it is. Chapter 5 discusses how to work around this.

The second is crypto/tls. TinyGo’s crypto/tls lacks some of the functions that Go’s standard library has. The OpenTelemetry OTLP exporter for Go refers to those functions even when you configure it without TLS. The build therefore fails, and Chapter 4 shows the actual error.

The third is the goroutine stack. In Go, a goroutine stack grows as needed. In TinyGo, its size is fixed at build time. The default on the ESP32-S3 is 8 KB, and you can change it with the -stack-size option. 8 KB is not enough to encode a deeply nested OTLP message with encoding/json. The only way to find the required size is to measure it on the real device. Chapters 6 and 9 present the results.

Network constraints

For the Wi-Fi connection, I use espradio. espradio is a wireless communication package for the ESP32 that the TinyGo project develops. For TCP/IP processing, I use lneto, a network stack written entirely in Go. DHCP, DNS, TCP, and UDP run as Go code on a microcontroller with no OS, and Go’s net package works too.

The trouble was that some features existed in the libraries but had no API that I could call from outside. lneto implements NTP. However, the standard connection procedure in espradio v0.3.0 does not configure an NTP server, and the function that runs NTP is out of reach from outside. OTLP data needs timestamps, so I wrote the clock synchronization myself (Chapter 8).

There was also no Go function that returns the signal strength (RSSI) of the connected access point. In Chapter 10, I call a function in the Wi-Fi driver binary directly.

The assumption of no TLS

The last constraint is that the device does not carry TLS. espradio v0.3.0 does not provide a TLS client, and TinyGo’s crypto/tls also lacks some functions. So in this book, the device sends OTLP over plain HTTP to a Collector on the same LAN. Encryption and authentication toward external backends are the Collector’s job.

This split has advantages too. The microcontroller does not need to hold backend credentials or certificates. You also do not need to rewrite the firmware when you change the backend. In exchange, the traffic between the device and the Collector is not encrypted, so you must keep both inside a trusted network. Chapter 11 covers the Collector setup.


  1. Ted Young is one of the co-founders of OpenTelemetry. At the time of writing, his GitHub profile says that he works at Grafana Labs. The quoted comment is from June 21, 2024. ↩︎

  2. The value comes from Espressif’s ESP32-S3 datasheet. ↩︎

  3. At the time of writing, PSRAM support for the ESP32-S3 is in progress in TinyGo as PR #5554. ↩︎