Skip to content

Commit 9dde253

Browse files
committed
Add OTEL instrumentation docs for Go
Signed-off-by: Han Verstraete (OpenFaaS Ltd) <[email protected]>
1 parent 379a2ae commit 9dde253

File tree

3 files changed

+248
-2
lines changed

3 files changed

+248
-2
lines changed

docs/edge/open-telemetry.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,4 @@ Checkout the docs for more details on [how to configure Grafana Alloy to collect
7676

7777
Functions and services deployed on OpenFaaS Edge can use the service name when configuring the telemetry collection endpoint e,g. `alloy:4317` or `alloy:4318`.
7878

79-
For more info on how to instrument and configure telemetry for functions checkout our language guides for: [Python](/languages/python/#opentelemetry-zero-code-instrumentation) or [Node.js](/languages/node/#opentelemetry-zero-code-instrumentation).
79+
For more info on how to instrument and configure telemetry for functions checkout our language guides for: [Python](/languages/python/#opentelemetry-zero-code-instrumentation), [Node.js](/languages/node/#opentelemetry-zero-code-instrumentation) or [Go](/languages/go/#opentelemetry-instrumentation)

docs/languages/go.md

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,3 +402,249 @@ func Handle(w http.ResponseWriter, r *http.Request) {
402402
}
403403
```
404404

405+
## OpenTelemetry instrumentation
406+
407+
To instrument a Go function it is recommended to modify one of the existing Go templates. This will allow you to instrument the HTTP server and handle shutdown for the OpenTelemetry SDK properly.
408+
409+
There are two ways to [customise a template](/cli/templates/#how-to-customise-a-template):
410+
411+
- Fork the template repository and modify the template. Recommended method that allows for distribution and reuse of the template.
412+
- Pull the template and apply patches directly in the `./template/<language_name>` directory. Good for quick iteration and experimentation with template modifications. The modified template cannot be shared and reused. Changes may get overwritten when pulling templates again.
413+
414+
In this example we are going to modify the [`golang-middleware` template](https://github.com/openfaas/golang-http-template).
415+
416+
Ensure the right packages are added in the template `go.mod`:
417+
418+
```sh
419+
go get go.opentelemetry.io/otel \
420+
go.opentelemetry.io/otel/trace \
421+
go.opentelemetry.io/otel/sdk \
422+
go.opentelemetry.io/otel/exporters/stdout/stdouttrace \
423+
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc \
424+
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp
425+
```
426+
427+
Modify the `main.go` file of the template to include code that sets up the OpenTelemetry SDK and instruments the HTTP server.
428+
429+
```diff
430+
func main() {
431+
+ ctx := context.Background()
432+
+
433+
+ // Create exporters by reading the exporter configuration from env variables.
434+
+ exporters, err := getExporters(ctx)
435+
+ if err != nil {
436+
+ log.Fatalf("failed to initialize exporters: %v", err)
437+
+ }
438+
+
439+
+ // Create a new tracer provider with a batch span processor and the given exporters.
440+
+ tp := newTracerProvider(exporters)
441+
+ otel.SetTracerProvider(tp)
442+
+
443+
+ // Handle shutdown properly so nothing leaks.
444+
+ defer func() { _ = tp.Shutdown(ctx) }()
445+
+
446+
readTimeout := parseIntOrDurationValue(os.Getenv("read_timeout"), defaultTimeout)
447+
writeTimeout := parseIntOrDurationValue(os.Getenv("write_timeout"), defaultTimeout)
448+
healthInterval := parseIntOrDurationValue(os.Getenv("healthcheck_interval"), writeTimeout)
449+
450+
s := &http.Server{
451+
Addr: fmt.Sprintf(":%d", 8082),
452+
ReadTimeout: readTimeout,
453+
WriteTimeout: writeTimeout,
454+
MaxHeaderBytes: 1 << 20, // Max header of 1MB
455+
}
456+
457+
- http.HandleFunc("/", function.Handle)
458+
+ http.Handle("/", otelhttp.NewHandler(http.HandlerFunc(function.Handle), ""))
459+
460+
listenUntilShutdown(s, healthInterval, writeTimeout)
461+
}
462+
```
463+
464+
The `getExporters` function creates and returns a list of exporters. The configuration for the exporter is read from environment variables. This makes it easy to later configure the exporters through the `stack.yaml` file of functions that use the template.
465+
466+
By default we support console and OTLP over gRPC exporters but you can modify this function to create your preferred exporters with specific configuration if that is required.
467+
468+
```go
469+
func getExporters(ctx context.Context) ([]sdktrace.SpanExporter, error) {
470+
var exporters []sdktrace.SpanExporter
471+
472+
exporterTypes := os.Getenv("OTEL_TRACES_EXPORTER")
473+
if exporterTypes == "" {
474+
exporterTypes = "stdout"
475+
}
476+
477+
insecureValue := os.Getenv("OTEL_EXPORTER_OTLP_TRACES_INSECURE")
478+
insecure, err := strconv.ParseBool(insecureValue)
479+
if err != nil {
480+
insecure = false
481+
}
482+
483+
for _, exp := range strings.Split(exporterTypes, ",") {
484+
switch exp {
485+
case "console":
486+
exporter, err := stdouttrace.New(stdouttrace.WithPrettyPrint())
487+
if err != nil {
488+
return nil, err
489+
}
490+
491+
exporters = append(exporters, exporter)
492+
case "otlp":
493+
endpoint := os.Getenv("OTEL_EXPORTER_OTLP_ENDPOINT")
494+
if endpoint == "" {
495+
endpoint = "0.0.0.0:4317"
496+
}
497+
498+
opts := []otlptracegrpc.Option{
499+
otlptracegrpc.WithEndpoint(endpoint),
500+
otlptracegrpc.WithDialOption(grpc.WithBlock()),
501+
}
502+
503+
if insecure {
504+
opts = append(opts, otlptracegrpc.WithInsecure())
505+
506+
}
507+
508+
exporter, err := otlptracegrpc.New(ctx, opts...)
509+
if err != nil {
510+
return nil, err
511+
}
512+
513+
exporters = append(exporters, exporter)
514+
default:
515+
fmt.Printf("unknown OTEL exporter type: %s", exp)
516+
}
517+
}
518+
519+
return exporters, nil
520+
}
521+
```
522+
523+
The `newTracerProvider` initializes a new tracer provider using the provided exporters.
524+
525+
```go
526+
func newTracerProvider(exporters []sdktrace.SpanExporter) *sdktrace.TracerProvider {
527+
serviceName := os.Getenv("OTEL_SERVICE_NAME")
528+
if serviceName == "" {
529+
serviceName = "golang-middleware-function"
530+
}
531+
532+
// Ensure default SDK resources and the required service name are set.
533+
r, err := resource.Merge(
534+
resource.Default(),
535+
resource.NewWithAttributes(
536+
semconv.SchemaURL,
537+
semconv.ServiceName(serviceName),
538+
),
539+
)
540+
541+
if err != nil {
542+
panic(err)
543+
}
544+
545+
opts := []sdktrace.TracerProviderOption{
546+
sdktrace.WithResource(r),
547+
}
548+
549+
for _, exporter := range exporters {
550+
processor := sdktrace.NewBatchSpanProcessor(exporter)
551+
opts = append(opts, sdktrace.WithSpanProcessor(processor))
552+
}
553+
554+
return sdktrace.NewTracerProvider(opts...)
555+
}
556+
```
557+
558+
Use the modified template to create a new function.
559+
560+
```sh
561+
faas-cli new echo --lang golang-middleware-otel
562+
```
563+
564+
Use environment variables to configure the traces exporter for the function.
565+
566+
```diff
567+
functions:
568+
greet:
569+
lang: python3-http-otel
570+
handler: ./greet
571+
image: greet:latest
572+
+ environment:
573+
+ OTEL_SERVICE_NAME: greet.${NAMESPACE:-openfaas-fn}
574+
+ OTEL_TRACES_EXPORTER: console,otlp
575+
+ OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-collector:4317}
576+
```
577+
578+
- `OTEL_SERVICE_NAME` sets the name of the service associated with the telemetry and is used to identify telemetry for a specific function. It can be set to any value you want but we recommend using the clear function identifier `<fn-name>.<fn-namespace>`.
579+
- `OTEL_TRACES_EXPORTER` specifies which tracer exporter to use. In this example traces are exported to `console` (stdout) and with `otlp` to send traces to an endpoint that accepts OTLP via gRPC.
580+
- `OTEL_EXPORTER_OTLP_ENDPOINT` sets the endpoint where telemetry is exported to.
581+
582+
### Creating custom traces in the function handler
583+
584+
When using a modified OpenTelemetry template the global trace provider can be retrieved to add custom spans or additional instrumentation libraries in the function handler.
585+
586+
Make sure the install the required packages:
587+
588+
```sh
589+
go get "go.opentelemetry.io/otel" \
590+
"go.opentelemetry.io/otel/trace"
591+
```
592+
593+
Add custom spans in the function handler:
594+
595+
```go
596+
package function
597+
598+
import (
599+
"fmt"
600+
"io"
601+
"net/http"
602+
"sync"
603+
604+
"go.opentelemetry.io/otel"
605+
)
606+
607+
var tracer trace.Tracer
608+
var mu sync.Mutex
609+
610+
func getTracer() trace.Tracer {
611+
if tracer == nil {
612+
mu.Lock()
613+
tracer = otel.GetTracerProvider().Tracer("function")
614+
mu.Unlock()
615+
}
616+
617+
return tracer
618+
}
619+
620+
621+
func Handle(w http.ResponseWriter, r *http.Request) {
622+
ctx, span := getTracer().Start(r.Context(), "function-span")
623+
defer span.End()
624+
625+
var input []byte
626+
627+
if r.Body != nil {
628+
defer r.Body.Close()
629+
630+
body, _ := io.ReadAll(r.Body)
631+
632+
input = body
633+
}
634+
635+
w.WriteHeader(http.StatusOK)
636+
w.Write([]byte(fmt.Sprintf("Body: %s", string(input))))
637+
}
638+
```
639+
640+
The function `otel.GetTracerProvider()` can be used to get the registered trace provider and create a new named tracer for the function.
641+
642+
To create new span with a tracer, you’ll need a handle on a `context.Context` instance. These will typically come from the request object and may already contain a parent span from an [instrumentation library](https://opentelemetry.io/docs/languages/go/libraries/).
643+
644+
Checkout the [OpenTelemetry docs](https://opentelemetry.io/docs/languages/go/instrumentation/) for more information on how to work with spans.
645+
646+
## OpenTelemetry zero-code instrumentation
647+
648+
The beta Release for [zero-code instrumentation](https://opentelemetry.io/docs/concepts/instrumentation/zero-code/) of Go applications [has recently been announced](https://opentelemetry.io/blog/2025/go-auto-instrumentation-beta/).
649+
650+
Go auto-instrumentation has not been tested with OpenFaaS functions yet. The easiest way to try it out on Kubernetes is probably by using the [Opentelemetry Operator for Kubernetes](https://opentelemetry.io/docs/platforms/kubernetes/operator/automatic/).

docs/languages/python.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,7 +442,7 @@ functions:
442442
+ OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-collector:4317}
443443
```
444444

445-
- `OTEL_SERVICE_NAME` sets the name of the service associated with the telemetry and is used to identify telemetry for a specific function. It can be set to any value you want be we recommend using the clear function identifier `<fn-name>.<fn-namespace>`.
445+
- `OTEL_SERVICE_NAME` sets the name of the service associated with the telemetry and is used to identify telemetry for a specific function. It can be set to any value you want but we recommend using the clear function identifier `<fn-name>.<fn-namespace>`.
446446
- `OTEL_TRACES_EXPORTER` specifies which tracer exporter to use. In this example traces are exported to `console` (stdout) and with `otlp`. The `otlp` option tells `opentelemetry-instrument` to send the traces to an endpoint that accepts OTLP via gRPC.
447447
- setting `OTEL_METRICS_EXPORTER` and `OTEL_LOGS_EXPORTER` to `none` we disable the metrics and logs exporters. You can enable them if desired.
448448
- `OTEL_EXPORTER_OTLP_ENDPOINT` sets the endpoint where telemetry is exported to.

0 commit comments

Comments
 (0)