Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic authorization #95

Open
christiangroth opened this issue Mar 16, 2021 · 20 comments
Open

Basic authorization #95

christiangroth opened this issue Mar 16, 2021 · 20 comments

Comments

@christiangroth
Copy link

Hi,

I'm trying to get a basic auth to work, unfortunately I'm not successful. Here is what I've done so far:

Installed Ktor Authorization feature:

    install(Authentication) {
        basic {
            realm = BuildProperties.application
            validate { credential ->
                if(credential.name == credential.password) {
                    UserIdPrincipal(credential.name)
                } else {
                    null
                }
            }
        }
    }

Implemented my AuthProvider:

object BasicAuthProvider : AuthProvider<UserIdPrincipal> {
    override val security =
        listOf(
            listOf(
                AuthProvider.Security(
                    SecuritySchemeModel(
                        name = "basicAuth",
                        type = SecuritySchemeType.http,
                        scheme = HttpSecurityScheme.basic,
                    ), emptyList<Scopes>()
                )
            )
        )

    override suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): UserIdPrincipal {
        return pipeline.context.authentication.principal() ?: throw RuntimeException("No UserIdPrincipal")
    }

    override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
        val authenticatedKtorRoute = route.ktorRoute.authenticate { }
        return OpenAPIAuthenticatedRoute(authenticatedKtorRoute, authProvider = this)
    }
}

enum class Scopes(override val description: String) : Described {
    Profile("Some scope")
}

Implemented shortcut extension function for routing definitions:

inline fun NormalOpenAPIRoute.auth(route: OpenAPIAuthenticatedRoute<UserIdPrincipal>.() -> Unit): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
    return BasicAuthProvider.apply(this).apply { route() }
}

Enhanced an existing route to use basic auth:

fun NormalOpenAPIRoute.versionApi() {
    route("version") {
        auth {
            get<Unit, VersionResponse>(
                EndpointInfo("Version info", "Returns information about the current version this service runs in."),
                tags(Metadata),
            ) {
                pipeline.call.respond(VersionResponse())
            }
        }
    }
}

The result is, that my /version route exists, is listed in swagger-ui / openapi.json but is not authenticated. Also I don't get auth information in swagger-ui / openapi.json. I'm quite sure I'm close, but I don't get what I'm missing right now? Docs and examples are not that helpful at that point, neither existing issues ... or I just don't get it.

Thanks for your help, Chris

@christiangroth
Copy link
Author

Not sure if the output of /openapi.json is also helpful:

// 20210316155318
// http://localhost:8080/openapi.json

{
  "components": {
    "schemas": {
      "de.espirit.todoapp.VersionResponse": {
        "nullable": false,
        "properties": {
          "gitBranch": {
            "nullable": false,
            "type": "string"
          },
          "gitHash": {
            "nullable": false,
            "type": "string"
          },
          "version": {
            "nullable": false,
            "type": "string"
          }
        },
        "required": [
          "gitBranch",
          "gitHash",
          "version"
        ],
        "type": "object"
      }
    }
  },
  "info": {
    "description": "This API allows to manage simple todo items. \nAll items are kept in memory only, so restarting the service will result in data loss.\n\nPlease do not hesitate to contact the team in case of issues or questions.",
    "title": "HTTP Service Template",
    "version": "PX-48-SNAPSHOT"
  },
  "openapi": "3.0.0",
  "paths": {
    "/api/todos": {
      ...
    },
    "/api/todos/{id}": {
      ...
    },
    "/version": {
      "get": {
        "description": "Returns information about the current version this service runs in.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.VersionResponse"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "Version info",
        "tags": [
          "Metadata"
        ]
      }
    }
  },
  "tags": [
    {
      "description": "An API to manage simple todo items.",
      "name": "TODOs API"
    },
    {
      "description": "Several endpoints for requesting service metadata.",
      "name": "Metadata"
    }
  ]
}

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 16, 2021

maybe you need to inherit the route provider as well like this:

OpenAPIAuthenticatedRoute(this.ktorRoute.authenticate(authName) {}, this.provider.child(), this).throws(
    APIException.apiException<BadPrincipalException>(HttpStatusCode.Unauthorized)
)

Or use a named authenication.

you could also change .apply { route() } with .apply(route) but in theory it means the same.

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 16, 2021

Ah wait: you used route.ktorroute instead of this.ktorroute

@christiangroth
Copy link
Author

christiangroth commented Mar 16, 2021

Hi @Wicpar , thanks for your quick reply!

Ah wait: you used route.ktorroute instead of this.ktorroute

Yeah, because I'm inside AuthProvider and don't have this.ktorRoute there, right?
Edit: I think "your this" is "my route" ... ;) Because I use it that way

inline fun NormalOpenAPIRoute.auth(route: OpenAPIAuthenticatedRoute<UserIdPrincipal>.() -> Unit): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
    return BasicAuthProvider.apply(this).apply { route() }
}

maybe you need to inherit the route provider as well like this:

OpenAPIAuthenticatedRoute(this.ktorRoute.authenticate(authName) {}, this.provider.child(), this).throws(
    APIException.apiException<BadPrincipalException>(HttpStatusCode.Unauthorized)
)

Or use a named authenication.

you could also change .apply { route() } with .apply(route) but in theory it means the same.

Not 100% sure I already tried these, but I'll double check it in the office tomorrow.

@christiangroth
Copy link
Author

So, unfortunately neither the named authentication nor the inherited provider did change anything. Also changing .apply { route() } to .apply(route) did change anything.

A last suggestion that came to my mind is regarding the Ktor version. I saw you use 1.3.2 (https://github.com/papsign/Ktor-OpenAPI-Generator/blob/master/gradle.properties). I'm on 1.5.2. Are there any known incompatibilities?

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 17, 2021

Not to my knowledge

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 17, 2021

I have it setup like this:

class OAuth2Provider(scopes: List<T>) : AuthProvider<A> {
override suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): A =
    this@OAuth2Handler.getAuth(pipeline.call.principal()!!)

override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<A> =
    OpenAPIAuthenticatedRoute(route.ktorRoute.authenticate(authName) {}, route.provider.child(), this).throws(
        APIException.apiException<BadPrincipalException>(HttpStatusCode.Unauthorized)
    )

  override val security: Iterable<Iterable<AuthProvider.Security<*>>> =
      listOf(listOf(AuthProvider.Security(scheme, scopes)))
}

fun auth(apiRoute: NormalOpenAPIRoute, scopes: List<T>): OpenAPIAuthenticatedRoute<A> {
  val authProvider = OAuth2Provider(scopes)
  return authProvider.apply(apiRoute)
}

@christiangroth
Copy link
Author

christiangroth commented Mar 18, 2021

Found the error! It wasn't on the side of implementing AuthHandler, but on the usage side. Don't want to be too harsh here, but that's kind of bad API or at least really easy to mess up.

Here's the situation before:
image

And that's the fixed one:
image

So the issue was, the Import of the get method still pointed to normal package instead of auth. I would have expected an compile error, because inside the auth-Block this points to OpenAPIAuthenticatedRoute and not to NormalOpenAPIRoute.

Please don't get me wrong, you're still doing a great job and I like the project very much! Thank you very much for your quick feedback and help. I will now see that I implement my usecases, but I think nothing stands in the way now. :)

Edit: Of course the third generic parameter was also missing, but especially if you have normal and auth routes in one file, you need both imports and I bet at leat I will mess it up :D Using explicit this.get<....>(...) will lead to expected compile error, checked that, but implicit this won't.

@christiangroth
Copy link
Author

christiangroth commented Mar 18, 2021

Got some last minor questions, not sure if I may open separate issues for them, please just let me know:

  1. the route is now properly intercepted and authorized, also swagger-ui shows my BasicAuth as part of Available authorizations in the top. However, regarding the route, I see th symbol that it's authorized, but if I click it, the Available authorizations are empty.

Global:
image

Route:
image

  1. When declaring example for API throws, they are not used in OpanAPI as I would have expected.

Code :

@Serializable
data class ResponseError(val code: Int, val description: String, val message: String? = null) {
    constructor(statusCode: HttpStatusCode, message: String? = null) : this(statusCode.value, statusCode.description, message)
}

object BasicAuthProvider : AuthProvider<UserIdPrincipal> {

    [...]

    override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
        return OpenAPIAuthenticatedRoute(route.ktorRoute.authenticate { }, route.provider.child(), this)
            .throws(
                status = HttpStatusCode.Unauthorized,
                // TODO not used in openApi.json
                example = ResponseError(HttpStatusCode.Unauthorized, "Missing authorization to access this route."),
                gen = { e: UnauthorizedException -> return@throws ResponseError(HttpStatusCode.Unauthorized, e.message) }
            )
            .throws(
                status = HttpStatusCode.Forbidden,
                example = ResponseError(HttpStatusCode.Forbidden, "Insufficient access permissions for this route."),
                gen = { e: ForbiddenException -> return@throws ResponseError(HttpStatusCode.Forbidden, e.message) }
            )
    }
}

Result in Swagger-UI:
image

I would have expected the default values of created ResponseError instanced for example parameters to be present in the UI as well.

  1. Regarding the thows-Information from 2). Is it possible to define a custom description? Right now only the name of the Status Code is shown. For 401 and 403 it is quite obvious, but if one introduces let's say a HTTP 409 Conflict a more detailed description would be helpful for the API users.

EDIT: Added openapi.json

// 20210318092817
// http://localhost:8080/openapi.json

{
  "components": {
    "schemas": {
      "de.espirit.todoapp.ResponseError": {
        "nullable": false,
        "properties": {
          "code": {
            "format": "int32",
            "nullable": false,
            "type": "integer"
          },
          "description": {
            "nullable": false,
            "type": "string"
          },
          "message": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "code",
          "description"
        ],
        "type": "object"
      },
      "de.espirit.todoapp.Todo": {
        "nullable": false,
        "properties": {
          "id": {
            "nullable": false,
            "type": "string"
          },
          "text": {
            "nullable": true,
            "type": "string"
          },
          "title": {
            "nullable": false,
            "type": "string"
          }
        },
        "required": [
          "id",
          "title"
        ],
        "type": "object"
      },
      "de.espirit.todoapp.TodoRequestData": {
        "nullable": false,
        "properties": {
          "text": {
            "nullable": true,
            "type": "string"
          },
          "title": {
            "nullable": false,
            "type": "string"
          }
        },
        "required": [
          "title"
        ],
        "type": "object"
      },
      "de.espirit.todoapp.VersionResponse": {
        "nullable": false,
        "properties": {
          "gitBranch": {
            "nullable": false,
            "type": "string"
          },
          "gitHash": {
            "nullable": false,
            "type": "string"
          },
          "version": {
            "nullable": false,
            "type": "string"
          }
        },
        "required": [
          "gitBranch",
          "gitHash",
          "version"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "basicAuth": {
        "name": "basicAuth",
        "scheme": "basic",
        "type": "http"
      }
    }
  },
  "info": {
    "contact": {
      "email": "team-delivery_platform@e-spirit.com",
      "name": "Team Delivery Platform"
    },
    "description": "This API allows to manage simple todo items. \nAll items are kept in memory only, so restarting the service will result in data loss.\n\nPlease do not hesitate to contact the team in case of issues or questions.",
    "title": "HTTP Service Template",
    "version": "PX-48-SNAPSHOT"
  },
  "openapi": "3.0.0",
  "paths": {
    "/api/todos": {
      "post": {
        "description": "Creates a new todo with given data.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/de.espirit.todoapp.TodoRequestData"
              }
            }
          }
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.Todo"
                }
              }
            },
            "description": "Created"
          }
        },
        "summary": "Create todos",
        "tags": [
          "TODOs API"
        ]
      },
      "get": {
        "description": "Lists all todos.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "items": {
                    "$ref": "#/components/schemas/de.espirit.todoapp.Todo"
                  },
                  "nullable": false,
                  "type": "array"
                }
              }
            },
            "description": "OK"
          }
        },
        "summary": "List todos",
        "tags": [
          "TODOs API"
        ]
      }
    },
    "/api/todos/{id}": {
      "get": {
        "description": "Retrieve the todo identified by path parameter, or HTTP 404 Not Found.",
        "parameters": [
          {
            "deprecated": false,
            "description": "Todo id path parameter",
            "explode": false,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "nullable": false,
              "type": "string"
            },
            "style": "simple"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.Todo"
                }
              }
            },
            "description": "OK"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.ResponseError"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "summary": "Retrieve todo",
        "tags": [
          "TODOs API"
        ]
      },
      "delete": {
        "description": "Deleted the todo identified by path parameter, or HTTP 404 Not Found.",
        "parameters": [
          {
            "deprecated": false,
            "description": "Todo id path parameter",
            "explode": false,
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "nullable": false,
              "type": "string"
            },
            "style": "simple"
          }
        ],
        "responses": {
          "204": {
            "description": "No Content"
          },
          "404": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.ResponseError"
                }
              }
            },
            "description": "Not Found"
          }
        },
        "summary": "Delete todo",
        "tags": [
          "TODOs API"
        ]
      }
    },
    "/version": {
      "get": {
        "description": "Returns information about the current version this service runs in.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.VersionResponse"
                }
              }
            },
            "description": "OK"
          },
          "401": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.ResponseError"
                }
              }
            },
            "description": "Unauthorized"
          },
          "403": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/de.espirit.todoapp.ResponseError"
                }
              }
            },
            "description": "Forbidden"
          }
        },
        "security": [
          {
            "entries": [
              null
            ],
            "keys": [
              "basicAuth"
            ],
            "size": 1,
            "values": [
              [
                
              ]
            ]
          }
        ],
        "summary": "Version info",
        "tags": [
          "Metadata"
        ]
      }
    }
  },
  "tags": [
    {
      "description": "An API to manage simple todo items.",
      "name": "TODOs API"
    },
    {
      "description": "Several endpoints for requesting service metadata.",
      "name": "Metadata"
    }
  ]
}

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 18, 2021

Ah yes Indeed...
There is no way around that in ktor sadly as you cannot stop scope inheritance.
The only way to change that is to change the syntax entirely to use an objection based method instead of fixed type parameters.

For your questions:
1: iirc it worked for oauth, maybe extra definitions are needed for basic auth
2: it could be a regression, can you try an earlier version from a few monts ago ?

@christiangroth
Copy link
Author

  1. Regarding to https://swagger.io/docs/specification/authentication/basic-authentication/ three things are needed:
  • an entry in components.securitySchemes (here the name attribute is generated, which should be the key of the entry, see screenshot)
  • an entry (empty array) in security with the same name (that's missing)
  • an entry in the security array under path.http-verb (the entry is there, but looks wrong, see screenshot)

image

  1. Tried 0.2-beta.8, 0.2-beta.10, 0.2-beta.13 .. does not work in any of these

Do you have any feedback on 3)?

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 18, 2021

Didn't see 3 you can simply edit the description in the status code class.

1 seems like a regression, i'll look into it
2 is an oversight then i'll look into it as well

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 18, 2021

@christiangroth 2 works properly when i copy your code, the one difference is that i don' t have a @serialize annotation so I removed it.
image

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 18, 2021

@christiangroth what is your jackson configuration for the server ?

install(io.ktor.features.ContentNegotiation) {
            jackson {
                enable(
                    com.fasterxml.jackson.databind.DeserializationFeature.WRAP_EXCEPTIONS,
                    com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_INTEGER_FOR_INTS,
                    com.fasterxml.jackson.databind.DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
                )

                enable(com.fasterxml.jackson.databind.SerializationFeature.WRAP_EXCEPTIONS, com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT)

                setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)

                setDefaultPrettyPrinter(com.fasterxml.jackson.core.util.DefaultPrettyPrinter().apply {
                    indentArraysWith(com.fasterxml.jackson.core.util.DefaultPrettyPrinter.FixedSpaceIndenter.instance)
                    indentObjectsWith(com.fasterxml.jackson.core.util.DefaultIndenter("  ", "\n"))
                })

                registerModule(com.fasterxml.jackson.datatype.jsr310.JavaTimeModule())
            }
        }

@christiangroth
Copy link
Author

We don't use Jackson, but kotlinx-serialization instead. I took the code snippet from #42 to manage DataModel serialization.

@Wicpar
Copy link
Collaborator

Wicpar commented Mar 19, 2021

It looks like the origin of your issue, you'll have to debug that on your own as i know nothing of kotlinx/serialization.

@christiangroth
Copy link
Author

Yeah, I also thought that. I'll try to finde some time next week and come back to you / keep this issue updated.

@christiangroth
Copy link
Author

So I solved 2 and 3 by fixing my models. The DataModel inheritance was missing, so obviously I did not work for custom types.

If you have any updates on the possible regression regarding 1) within the next days, that would be fine. Thx :)

@hmmeral
Copy link

hmmeral commented Jun 7, 2021

@christiangroth hey, i am also trying to figure out this issue. Is it possible to provide your full example? Thanks

@christiangroth
Copy link
Author

christiangroth commented Jun 14, 2021

@hmmeral I'm not sure what you're missing, so I just copy the complete code again (just removed some internal details) :) Hope that helps.

Definition side:

import com.papsign.ktor.openapigen.model.Described
import com.papsign.ktor.openapigen.model.security.HttpSecurityScheme
import com.papsign.ktor.openapigen.model.security.SecuritySchemeModel
import com.papsign.ktor.openapigen.model.security.SecuritySchemeType
import com.papsign.ktor.openapigen.modules.providers.AuthProvider
import com.papsign.ktor.openapigen.route.path.auth.OpenAPIAuthenticatedRoute
import com.papsign.ktor.openapigen.route.path.normal.NormalOpenAPIRoute
import com.papsign.ktor.openapigen.route.throws
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.util.pipeline.*

const val BASIC_AUTHENTICATION_METRICS = "metrics"

fun Application.installAuthenticationFeature() {
    install(Authentication) {

        val metricsUser = environment.config.property(ConfigKey.METRICS_BASIC_AUTH_USER.path)?.getString()
        val metricsPassword = environment.config.property(ConfigKey.METRICS_BASIC_AUTH_PASSWORD.path)?.getString()
        basic(BASIC_AUTHENTICATION_METRICS) {
            realm = BuildProperties.application
            validate { credential ->
                if (credential.name == metricsUser && credential.password == metricsPassword) {
                    UserIdPrincipal(credential.name)
                } else {
                    null
                }
            }
        }
    }
}

inline fun NormalOpenAPIRoute.auth(route: OpenAPIAuthenticatedRoute<UserIdPrincipal>.() -> Unit): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
    return BasicAuthProvider.apply(this).apply(route)
}

class UnauthorizedException(message: String) : RuntimeException(message)
class ForbiddenException(message: String) : RuntimeException(message)

// even if we don't need scopes at all, an empty enum has to be there, see https://github.com/papsign/Ktor-OpenAPI-Generator/issues/65
enum class Scopes : Described

object BasicAuthProvider : AuthProvider<UserIdPrincipal> {

    // description for OpenAPI model
    override val security =
        listOf(
            listOf(
                AuthProvider.Security(
                    SecuritySchemeModel(
                        name = "basicAuth",
                        type = SecuritySchemeType.http,
                        scheme = HttpSecurityScheme.basic,
                    ), emptyList<Scopes>()
                )
            )
        )

    // gets auth information at runtime
    override suspend fun getAuth(pipeline: PipelineContext<Unit, ApplicationCall>): UserIdPrincipal {
        return pipeline.context.authentication.principal()
            ?: throw UnauthorizedException("Unable to verify given credentials, or credentials are missing.")
    }

    // convert normal route to authenticated route including OpenAPI meta information
    // TODO OpenAPI: Not listed as available auths at path level
    override fun apply(route: NormalOpenAPIRoute): OpenAPIAuthenticatedRoute<UserIdPrincipal> {
        return OpenAPIAuthenticatedRoute(route.ktorRoute.authenticate(BASIC_AUTHENTICATION_METRICS) { }, route.provider.child(), this)
            .throws(
                status = HttpStatusCode.Unauthorized.description("Your identity could not be verified."),
                example = ResponseError(HttpStatusCode.Unauthorized, "Missing authorization to access this route."),
                gen = { e: UnauthorizedException -> return@throws ResponseError(HttpStatusCode.Unauthorized, e.message) }
            )
            .throws(
                status = HttpStatusCode.Forbidden.description("Your access rights are insufficient."),
                example = ResponseError(HttpStatusCode.Forbidden, "Insufficient access permissions for this route."),
                gen = { e: ForbiddenException -> return@throws ResponseError(HttpStatusCode.Forbidden, e.message) }
            )
    }
}

And the usage side:

import com.papsign.ktor.openapigen.route.EndpointInfo
import com.papsign.ktor.openapigen.route.path.auth.get
import com.papsign.ktor.openapigen.route.path.normal.NormalOpenAPIRoute
import com.papsign.ktor.openapigen.route.response.respond
import com.papsign.ktor.openapigen.route.route
import com.papsign.ktor.openapigen.route.tags
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.metrics.micrometer.*
import io.micrometer.core.instrument.ImmutableTag
import io.micrometer.prometheus.PrometheusConfig
import io.micrometer.prometheus.PrometheusMeterRegistry
import io.micrometer.prometheus.PrometheusRenameFilter

val appMicrometerRegistry = PrometheusMeterRegistry(PrometheusConfig.DEFAULT)

fun Application.installMetricsFeature() {
    install(MicrometerMetrics) {
        registry = appMicrometerRegistry

        // we also want the build information to be part of the metrics, i.e. this helps showing versions on dashboards
        // see https://www.robustperception.io/exposing-the-software-version-to-prometheus
        registry.gauge("build_info", listOf(
            ImmutableTag("application", BuildProperties.application),
            ImmutableTag("gitBranch", BuildProperties.gitBranch),
            ImmutableTag("gitHash", BuildProperties.gitHash),
            ImmutableTag("version", BuildProperties.version),
        ), 1)
    }
}

fun NormalOpenAPIRoute.metricsApi() {
    route("metrics") {
        auth {
            get<Unit, String, UserIdPrincipal>(
                EndpointInfo("Metrics", "Returns information about the current metrics for this service instance."),
                tags(Metadata),
            ) {
                respond(appMicrometerRegistry.scrape())
            }
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants