Introduction to OpenAPI with Instana

September 10, 2020

Post

Everybody loves REST APIs. With such interfaces, developers can easily hook into external systems and exchange data, or trigger events. In this article, we will take a look into a mechanism that will allow anyone to both specify and rely on a strict API specification.

Prerequisites

Of course you can read this article without ever leaving the browser, but it’s a lot more fun when following along. Here’s the list of things you might want to have:

  • wget or a similar tool to download things from the internet
  • a current Java runtime environment (the java command should be on your PATH)
  • a golang version of 1.12 on your PATH
  • a text-editor of your choice

So what’s the problem with REST APIs?

Whenever we’re working with foreign REST APIs, we need to rely on the public documentation, that the vendor supplies. These documents need to be as accurate as possible. As time goes by, code will inevitably diverge from your documentation, as you will most probably enhance the featureset and fix conceptual issues.

With Instana, we have chosen to evolve our REST API over time, and without a big-bang. To be able to expose accurate documentation, we automatically publish machine-generated documentation on every release to our GitHub pages documentation.

In the document, there’s nothing fancy. An html documentation, with some code snippets and endpoint specifications – time to create code resembling our model into your consumer-code, right? – Not quite. There’s a quick and easy path to consume our API from within your system without wrestling too much with our data-formats.

Enter OpenAPI

OpenAPI (the artist formerly known as “Swagger Specification”) is a project that aims to provide a path to happiness for both API consumers and vendors. By designing the specification rather than the endpoints directly, they aim at having a stable and binding contract between vendors and consumers.

What does such specification look like? Here we have a shortened version of a specification for the infamous “PetShop” application:

openapi: "3.0.0"
info:
  version: 1.0.0
  title: Swagger Petstore
  license:
    name: MIT
servers:
  - url: http://petstore.swagger.io/v1
paths:
  /pets:
    get:
      summary: List all pets
      operationId: listPets
      tags:
        - pets
      parameters:
        - name: limit
          in: query
          description: How many items to return at one time (max 100)
          required: false
          schema:
            type: integer
            format: int32
      responses:
        '200':
          description: A paged array of pets
          headers:
            x-next:
              description: A link to the next page of responses
              schema:
                type: string
          content:
            application/json:    
              schema:
                $ref: "#/components/schemas/Pets"
components:
  schemas:
    Pet:
      type: object
      required:
        - id
        - name
      properties:
        id:
          type: integer
          format: int64
        name:
          type: string
        tag:
          type: string
    Pets:
      type: array
      items:
        $ref: "#/components/schemas/Pet"

You might already have glimpsed at the possible consequences for the API in question. It says we have an endpoint /pets with a possible GET HTTP operation on it that returns a paginated list of Pets. When we now look up the Pets object in the components section, we can quickly infer that it is an array of Pet objects. The Pet object ist specified as well and we can see that a Pet has mandatory name and idproperties with associate types.

If we are able to read such specification with our own eyes – why cant machines?

Entering OpenAPI Generator

In the OpenAPI ecosystem, there are multiple projects focussed on processing specification documents. These range from validating documents, to creating mock servers and testing utilities to generating client and even server code. The one we’ll focus about in this article, is the OpenAPI generator project.

The OpenAPI generator project focusses on providing a convenient way to generate code you can work with from a given specification document from a single command.

Instana OpenAPI specification

To spice things up a bit, we will be working with our API specification. You can retrieve it through the generated documentation. (YAML Version / JSON Version). This document is updated regularly within our release cycle and you should be able to work with it right out of the box.

Enter Golang

The Go language is  great for creating small binaries for almost every occasion in a straight-forward fashion. We’ll use that and we require at least version 1.12 for proper modules support.

Implementation

First, we want to create our project directory, download the OpenAPI generator, and see what it can do for us:

# create and enter the project directory
$ mkdir instana-golang-example && cd $_
# download the jar
$ wget https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/5.0.0-beta/openapi-generator-cli-5.0.0-beta.jar \
  -O openapi-generator-cli.jar

# take peek at the different generators it offers us
$ java -jar openapi-generator-cli.jar

Impressive list, right? Next we’ll use that jar file to generate a go client for the Instana REST API:

# download the Instana openapi spec
$ mkdir resources && wget https://instana.github.io/openapi/openapi.yaml -O resources/openapi.yaml
# generated code is ugly, we'll use "gofmt" to format it in the same step
$ GO_POST_PROCESS_FILE="gofmt -s -w" java -jar openapi-generator-cli.jar generate -i resources/openapi.yaml -g go \
    -o pkg/instana \
    --additional-properties packageName=instana \
    --additional-properties isGoSubmodule=true \
    --additional-properties packageVersion=1.185.824 \
    --type-mappings=object=interface{} \
    --enable-post-process-file \
    --skip-validate-spec

Done. Don’t believe me? Let’s have a look into the generated documentation:

cat pkg/instana/README.md

We can now use it in our tiny new tool. Let’s create a go.mod file in the current directory to include the library in our project:

module github.com/instana/instana-openapi-golang-example

require github.com/instana/instana-openapi-golang-example/pkg/instana v1.185.824

replace github.com/instana/instana-openapi-golang-example/pkg/instana v1.185.824 => ./pkg/instana

Cool. We just linked our local api client into the go modules dependency management. Go modules are awesome!

To show you how to use the client, we can create a small program that will query all the available search fields you can use in instana.

Let’s create a main.go file in our current directory:

package main

import (
	"context"
	"fmt"
	"os"
	"strings"
	"time"

	"github.com/antihax/optional"
	"github.com/instana/instana-openapi-golang-example/pkg/instana"
)

func main() {
	// grab settings from environment
	apiKey := os.Getenv("INSTANA_KEY")
	tenant := os.Getenv("INSTANA_TENANT")
	unit := os.Getenv("INSTANA_UNIT")

	// create golang client specific configuration
	configuration := instana.NewConfiguration()
	host := fmt.Sprintf("%s-%s.instana.io", unit, tenant)
	configuration.Host = host
	configuration.BasePath = fmt.Sprintf("https://%s", host)

	client := instana.NewAPIClient(configuration)

	// Instana uses the `apiToken` prefix in the `Authorization` header
	auth := context.WithValue(context.Background(), instana.ContextAPIKey, instana.APIKey{
		Key:    apiKey,
		Prefix: "apiToken",
	})

	searchFieldResults, _, err := client.InfrastructureCatalogApi.GetInfrastructureCatalogSearchFields(auth)
	if err != nil {
		fmt.Println(err)

		os.Exit(1)
	}

	fmt.Println("Available search fields supported by Instana:")
	for _, field := range searchFieldResults {
		fmt.Println(fmt.Sprintf("%s (%s) - %s", field.Keyword, field.Context, field.Description))
	}
}

Next, we need an API token. To get yourself a shiny new token, head to your Instana instance, and got to Settings -> API Tokens -> Add API Token:

API token creation with Instana

Side note: we need the tenant and unit from your environment. When using our SaaS offering, you can get these from yor environments’ URL. If you are accessing your environment through https://qa-acme.instana.io, then acme is the tenant and qa is the unit.

That’s it. Let’s run our code and see what search fields we can use with Instana:

$ INSTANA_TENANT=acme INSTANA_UNIT=qa INSTANA_KEY=$your_token$ go run main.go

Available search fields supported by Instana:
entity.kubernetes.pod.name (entities) - Kubernetes Pod Name
event.specification.id (events) - Event specification ID
entity.ping.target (entities) - Ping target
entity.azure.service.sqlserver.name (entities) - The name of the SQL database server
entity.lxc.ip (entities) - LXC container IP
entity.jboss.serverName (entities) - JBoss Server Name
...

Making use of it

Listing the available tags is one thing, but nothing fancy, right? We want to live up to the hype and get metrics from a specific Kubernetes pod. Specifically the CPU requests over time.

To facilitate this, we need to find what we’re searching for. Instana organizes metrics by “plugins”. To see what plugins are available, we can query them:

// search for the kubernetes pod plugin
// https://instana.github.io/openapi/#operation/getInfrastructureCatalogPlugins
plugins, _, err := client.InfrastructureCatalogApi.GetInfrastructureCatalogPlugins(auth)
if err != nil {
	println(fmt.Errorf("Error reading plugins: %s", err))

	os.Exit(1)
}

println("Let's find ourselves some kubernetes pods!")

// search for kubernetes plugins:
for _, plugin := range plugins {
	if strings.Contains(plugin.Plugin, "kubernetes") {
		fmt.Printf("Found Kubernetes plugin %s with ID %s\n", plugin.Label, plugin.Plugin)
	}
}

The output will be very similar to this:

Metric for plugin `kubernetesPod`: Containers (ID: container_count)
Metric for plugin `kubernetesPod`: Memory Requests (ID: memoryRequests)
Metric for plugin `kubernetesPod`: CPU Limits (ID: cpuLimits)
Metric for plugin `kubernetesPod`: CPU Requests (ID: cpuRequests)
Metric for plugin `kubernetesPod`: Restarts (ID: restartCount)
Metric for plugin `kubernetesPod`: Memory Limits (ID: memoryLimits)

There you go. Our metric ID for getting CPU requests from the “kubernetesPod” plugin is “cpuRequests”.

Finding your snapshot

Next thing, knowing what metric from which plugin we want, we need to find Kubernetes pods in our infrastructure inventory. Instana organizes infrastructure changes over time in a concept called “snapshot”. We need to find a snapshot that contains “kubernetesPod” entities and then we can go query those metrics.

When you deploy our Instana Agent via one of the supported methods to your Kubernetes cluster, the agent pods themselves, will have names in the form of  “instana-agent-ID”. Knowing this, we can search for all snapshots that contain entities with values of instana-agent-* in the entity.kubernetes.pod.name field.

Same as you would query via Dynamic Focus Query:

// let's find all the Snapshots that involve Kubernetes pods!
// https://instana.github.io/openapi/#operation/getSnapshots
snapshotsWithKubernetesPods, _, err := client.InfrastructureMetricsApi.GetSnapshots(auth, &instana.GetSnapshotsOpts{
	Plugin: optional.NewString(kubernetesPodPluginID),

	// We know that clusters, when monitored with Instana usually have pods with a name of `instana-agent-*`
	Query: optional.NewString("entity.kubernetes.pod.name:instana-agent*"),

	// We can travel through time and query data from entities that are no more!
	Offline: optional.NewBool(true),

	// Instana uses Milliseconds accross the board
	WindowSize: optional.NewInt64(7 * 86400 * 1000),
	To:         optional.NewInt64(time.Now().Unix() * int64(time.Millisecond)),
})
if err != nil {
	println(fmt.Errorf("Error reading snapshots: %s", err))

	os.Exit(1)
}

for _, snapshot := range snapshotsWithKubernetesPods.Items {
	fmt.Printf("Kubernetes Pod %s on Host %s with snapshot ID %s \n", snapshot.Label, snapshot.Host, snapshot.SnapshotId)
}

The output will look like this:

Let's find a Kubernetes Pod that contains an Instana Agent
Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) on Host with snapshot ID fkHp5kkCvpBonSAkyZD03GagAL4
Kubernetes Pod instana-agent/instana-agent-1-7jlwd (pod) on Host with snapshot ID 2sCSGsxJGUJB3mpaQ7SVOHJlMmY
Kubernetes Pod instana-agent/instana-agent-1-7rqvm (pod) on Host with snapshot ID FU7AaxMkghV9vsoB05AEXdrZqpE

Retrieving the metrics

Now that we know the snapshot IDs, we can actually go and fetch the metrics from those snapshots:

println("Let's put everything together: Querying the cpuRequests pod metrics from a specific snapshot")

metricID := "cpuRequests"
metricsQuery := instana.GetCombinedMetrics{
	Plugin: kubernetesPodPluginID,
	Metrics: []string{
		metricID,
	},

	SnapshotIds: []string{
		snapshotsWithKubernetesPods.Items[0].SnapshotId,
	},

	TimeFrame: instana.TimeFrame{
		To:         time.Now().Unix() * 1000,
		WindowSize: 300000,
	},

	// 5 Minutes
	Rollup: 60,
}

metricsResult, _, err := client.InfrastructureMetricsApi.GetInfrastructureMetrics(auth, &instana.GetInfrastructureMetricsOpts{
	GetCombinedMetrics: optional.NewInterface(metricsQuery),
})

if err != nil {
	println(fmt.Errorf("Error reading metrics: %s", err.(instana.GenericOpenAPIError)))

	os.Exit(1)
}

for _, metric := range metricsResult.Items {
	for _, bracket := range metric.Metrics[metricID] {
		parsedTime := time.Unix(0, int64(bracket[0])*int64(time.Millisecond))
		fmt.Printf("CPU requests of Kubernetes Pod %s at %s: %f\n", snapshotsWithKubernetesPods.Items[0].Label, parsedTime, bracket[1])
	}
}

The output will look like this:

Let's put everything together: Querying the cpuRequests pod metrics from a specific snapshot
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:56:06.656 +0000 UTC: 0.599900
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:58:17.728 +0000 UTC: 0.599900
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 10:58:17.728 +0000 UTC: 0.599900
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:00:28.8 +0000 UTC: 0.599900
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:00:28.8 +0000 UTC: 0.599900
CPU requests of Kubernetes Pod instana-agent/instana-agent-1-fn69h (pod) at 2020-09-03 11:02:39.872 +0000 UTC: 0.599900

With having read until here, you learned how you can

  • create or rather generate a API client in GoLang through an OpenAPI specification
  • use that client to query the Instana REST API in a type-safe way
  • resolve metric values from a specific item from Instanas vast, automatically created infrastructure store

Summary

In the article, we learned how to generate an API client for a specific programming environment based on an OpenAPI specification that is provided by a 3rd party. This is adaptable for many different projects and languages. An upside of this approach is, that the contract is basically also the code that is being executed to communicate with the remote API.

In case of the Instana REST API, the OpenAPI makes it easy for us, and our customers to version our API specification and separate our on-premise product and our SaaS offering, so you can always generate a client library for your specific environment.

The complete code is available on GitHub.

 

Play with Instana’s APM Observability Sandbox

Developer, Product
Go, or like many people also call it Golang, is a general-purpose language created and backed by Google, but developed in the open. Started back in 2009, Go reached version 1.0 in...
|
Profiling
Monitoring is a critical component of literally any application stack. Without the ability to see how Go applications perform and get notified in case of an availability or performance problem, it would...
|
Profiling
The main focus of the development of version 2.0 of the Go agent was to improve reliability and reduce agent overhead by upgrading and tuning the profiling capabilities. Editor's Note: Instana's AutoProfile™...
|

Start your FREE TRIAL today!

As the leading provider of Automatic Application Performance Monitoring (APM) solutions for microservices, Instana has developed the automatic monitoring and AI-based analysis DevOps needs to manage the performance of modern applications. Instana is the only APM solution that automatically discovers, maps and visualizes microservice applications without continuous additional engineering. Customers using Instana achieve operational excellence and deliver better software faster. Visit https://www.instana.com to learn more.