diff options
Diffstat (limited to 'kamon-core/src/main/scala/kamon')
7 files changed, 302 insertions, 12 deletions
diff --git a/kamon-core/src/main/scala/kamon/Kamon.scala b/kamon-core/src/main/scala/kamon/Kamon.scala index ab95d773..99327286 100644 --- a/kamon-core/src/main/scala/kamon/Kamon.scala +++ b/kamon-core/src/main/scala/kamon/Kamon.scala @@ -15,6 +15,9 @@ package kamon +import com.typesafe.config.{Config, ConfigRenderOptions} +import kamon.module.Module + object Kamon extends ClassLoading with Configuration with Utilities @@ -22,7 +25,8 @@ object Kamon extends ClassLoading with Tracing with ModuleLoading with ContextPropagation - with ContextStorage { + with ContextStorage + with StatusPage { @volatile private var _environment = Environment.fromConfig(config()) @@ -34,3 +38,21 @@ object Kamon extends ClassLoading _environment = Environment.fromConfig(config) }) } + + +object QuickTest extends App { + Kamon.loadModules() + Kamon.registerModule("my-module", new Module { + override def start(): Unit = {} + override def stop(): Unit = {} + override def reconfigure(newConfig: Config): Unit = {} + }) + + + //println("JSON CONFIG: " + Kamon.config().root().render(ConfigRenderOptions.concise().setFormatted(true).setJson(true))) + + + Thread.sleep(100000000) + + +} diff --git a/kamon-core/src/main/scala/kamon/ModuleLoading.scala b/kamon-core/src/main/scala/kamon/ModuleLoading.scala index 8fe035d6..bc90c656 100644 --- a/kamon-core/src/main/scala/kamon/ModuleLoading.scala +++ b/kamon-core/src/main/scala/kamon/ModuleLoading.scala @@ -36,7 +36,7 @@ trait SpanReporter extends kamon.module.SpanReporter { } * */ trait ModuleLoading { self: ClassLoading with Configuration with Utilities with Metrics with Tracing => - private val _moduleRegistry = new Module.Registry(self, self, clock(), self.metricRegistry(), self.tracer()) + protected val _moduleRegistry = new Module.Registry(self, self, clock(), self.metricRegistry(), self.tracer()) self.onReconfigure(newConfig => self._moduleRegistry.reconfigure(newConfig)) diff --git a/kamon-core/src/main/scala/kamon/StatusPage.scala b/kamon-core/src/main/scala/kamon/StatusPage.scala new file mode 100644 index 00000000..6f382a85 --- /dev/null +++ b/kamon-core/src/main/scala/kamon/StatusPage.scala @@ -0,0 +1,61 @@ +package kamon + +import com.typesafe.config.Config +import kamon.status.{StatusPageServer, Status} + +trait StatusPage { self: Configuration with ClassLoading with ModuleLoading with Configuration => + @volatile private var _statusPageServer: Option[StatusPageServer] = None + private val _status = new Status(self._moduleRegistry, self) + + // Initial configuration and reconfigures + init(self.config()) + self.onReconfigure(newConfig => self.init(newConfig)) + + + /** + * Allows to access internal Kamon status for troubleshooting and displaying purposes. All information returned + * by the Status instance is a immutable snapshot of the current state of a given component. + */ + def status(): Status = + _status + + + private def init(config: Config): Unit = synchronized { + val isStatusPageEnabled = config.getBoolean("kamon.status.enabled") + + if(isStatusPageEnabled) { + val hostname = config.getString("kamon.status.listen.hostname") + val port = config.getInt("kamon.status.listen.port") + + _statusPageServer.fold { + // Starting a new server on the configured hostname/port + startServer(hostname, port, self.classLoader()) + + }(existentServer => { + // If the configuration has changed we will stop the previous version + // and start a new one with the new hostname/port. + + if(existentServer.getHostname != hostname || existentServer.getListeningPort != port) { + stopServer() + startServer(hostname, port, self.classLoader()) + } + }) + + } else { + _statusPageServer.foreach(_ => stopServer()) + } + } + + private def startServer(hostname: String, port: Int, resourceLoader: ClassLoader): Unit = { + val server = new StatusPageServer(hostname, port, resourceLoader, _status) + server.start() + + _statusPageServer = Some(server) + } + + private def stopServer(): Unit = { + _statusPageServer.foreach(_.stop()) + _statusPageServer = None + } + +} diff --git a/kamon-core/src/main/scala/kamon/module/Module.scala b/kamon-core/src/main/scala/kamon/module/Module.scala index 41649629..f32e949b 100644 --- a/kamon-core/src/main/scala/kamon/module/Module.scala +++ b/kamon-core/src/main/scala/kamon/module/Module.scala @@ -289,19 +289,25 @@ object Module { } filter(_.isSuccess) map(_.get) toSeq - // Legacy modules from <1.2.0 - val legacyModules = config.getStringList("kamon.reporters").asScala map { moduleClass => - Settings(moduleClass, moduleClass, true) - } toSeq - val (repeatedLegacyModules, uniqueLegacyModules) = legacyModules.partition(lm => moduleSettings.find(_.fqcn == lm.fqcn).nonEmpty) - repeatedLegacyModules.foreach(m => - _logger.warn(s"Module [${m.name}] is configured twice, please remove it from the deprecated kamon.reporters setting.")) + // Load all modules that might have been configured using the legacy "kamon.reporters" setting from <1.2.0 + // versions. This little hack should be removed by the time we release 2.0. + // + if(config.hasPath("kamon.reporters")) { + val legacyModules = config.getStringList("kamon.reporters").asScala map { moduleClass => + Settings(moduleClass, moduleClass, true) + } toSeq - uniqueLegacyModules.foreach(m => - _logger.warn(s"Module [${m.name}] is configured in the deprecated kamon.reporters setting, please consider moving it to kamon.modules.")) + val (repeatedLegacyModules, uniqueLegacyModules) = legacyModules.partition(lm => moduleSettings.find(_.fqcn == lm.fqcn).nonEmpty) + repeatedLegacyModules.foreach(m => + _logger.warn(s"Module [${m.name}] is configured twice, please remove it from the deprecated kamon.reporters setting.")) - moduleSettings ++ uniqueLegacyModules + uniqueLegacyModules.foreach(m => + _logger.warn(s"Module [${m.name}] is configured in the deprecated kamon.reporters setting, please consider moving it to kamon.modules.")) + + moduleSettings ++ uniqueLegacyModules + + } else moduleSettings } /** @@ -322,6 +328,26 @@ object Module { /** + * Returns the current status of this module registry. + */ + private[kamon] def status(): Registry.Status = { + val automaticallyAddedModules = readModuleSettings(configuration.config()).map(moduleSettings => { + val entry = _registeredModules.get(moduleSettings.name) + Registry.ModuleInfo(moduleSettings.name, moduleSettings.fqcn, moduleSettings.enabled, entry.nonEmpty) + }) + + val programmaticallyAddedModules = _registeredModules + .filter { case (_, entry) => entry.programmaticallyAdded } + .map { case (name, entry) => { + Registry.ModuleInfo(name, entry.module.getClass.getName, true, true) + }} + + val allModules = automaticallyAddedModules ++ programmaticallyAddedModules + Registry.Status(allModules) + } + + + /** * Registers a module and schedules execution of its start procedure. */ private def startModule(entry: Entry[Module]): Unit = { @@ -396,6 +422,20 @@ object Module { } } + object Registry { + + case class Status( + modules: Seq[ModuleInfo] + ) + + case class ModuleInfo( + name: String, + description: String, + enabled: Boolean, + started: Boolean + ) + } + private def readRegistryConfiguration(config: Config): RegistrySettings = RegistrySettings( metricTickInterval = config.getDuration("kamon.metric.tick-interval"), diff --git a/kamon-core/src/main/scala/kamon/status/JsonMarshalling.scala b/kamon-core/src/main/scala/kamon/status/JsonMarshalling.scala new file mode 100644 index 00000000..c7c480fd --- /dev/null +++ b/kamon-core/src/main/scala/kamon/status/JsonMarshalling.scala @@ -0,0 +1,65 @@ +package kamon.status + +import com.grack.nanojson.JsonWriter +import kamon.module.Module +import kamon.module.Module.Registry +import java.lang.{StringBuilder => JavaStringBuilder} + +import com.typesafe.config.ConfigRenderOptions + + +trait JsonMarshalling[T] { + + /** + * Implementations should append a Json object or array that describes the given instance members and any + * additional information that is expected to be shown in the status mini site. + */ + def toJson(instance: T, builder: JavaStringBuilder): Unit +} + +object JsonMarshalling { + + implicit object ModuleRegistryStatusJsonMarshalling extends JsonMarshalling[Module.Registry.Status] { + override def toJson(instance: Registry.Status, builder: JavaStringBuilder): Unit = { + val array = JsonWriter.on(builder) + .`object`() + .array("modules") + + instance.modules.foreach(m => { + array.`object`() + .value("name", m.name) + .value("description", m.description) + .value("enabled", m.enabled) + .value("started", m.started) + .end() + }) + + array.end().end().done() + } + } + + implicit object BaseInfoJsonMarshalling extends JsonMarshalling[Status.BaseInfo] { + override def toJson(instance: Status.BaseInfo, builder: JavaStringBuilder): Unit = { + val baseConfigJson = JsonWriter.on(builder) + .`object`() + .value("version", instance.version) + .value("config", instance.config.root().render(ConfigRenderOptions.concise())) + + baseConfigJson.`object`("environment") + .value("service", instance.environment.service) + .value("host", instance.environment.host) + .value("instance", instance.environment.instance) + .`object`("tags") + + instance.environment.tags.foreach { + case (key, value) => baseConfigJson.value(key, value) + } + + baseConfigJson + .end() // ends tags + .end() // ends environment + .end() // ends base config + .done() + } + } +}
\ No newline at end of file diff --git a/kamon-core/src/main/scala/kamon/status/Status.scala b/kamon-core/src/main/scala/kamon/status/Status.scala new file mode 100644 index 00000000..f212ff55 --- /dev/null +++ b/kamon-core/src/main/scala/kamon/status/Status.scala @@ -0,0 +1,37 @@ +package kamon.status + +import com.typesafe.config.Config +import kamon.{Configuration, Environment, Kamon} +import kamon.module.Module + + +/** + * Allows accessing of component's status APIs without exposing any other internal API from those components. + */ +class Status(_moduleRegistry: Module.Registry, configuration: Configuration) { + + def baseInfo(): Status.BaseInfo = + Status.BaseInfo(BuildInfo.version, Kamon.environment, configuration.config()) + + /** + * Information about what modules have been detected in the classpath and their current status. + */ + def moduleRegistry(): Module.Registry.Status = + _moduleRegistry.status() +} + + + + +object Status { + + case class BaseInfo( + version: String, + environment: Environment, + config: Config + ) + + + + +} diff --git a/kamon-core/src/main/scala/kamon/status/StatusPageServer.scala b/kamon-core/src/main/scala/kamon/status/StatusPageServer.scala new file mode 100644 index 00000000..4e2bf03c --- /dev/null +++ b/kamon-core/src/main/scala/kamon/status/StatusPageServer.scala @@ -0,0 +1,65 @@ +package kamon.status + +import fi.iki.elonen.NanoHTTPD +import fi.iki.elonen.NanoHTTPD.Response +import fi.iki.elonen.NanoHTTPD.Response.{Status => StatusCode} + +class StatusPageServer(hostname: String, port: Int, resourceLoader: ClassLoader, status: Status) + extends NanoHTTPD(hostname, port) { + + private val RootResourceDirectory = "status" + private val ResourceExtensionRegex = ".*\\.([a-zA-Z]*)".r + + + override def serve(session: NanoHTTPD.IHTTPSession): NanoHTTPD.Response = { + if(session.getMethod() == NanoHTTPD.Method.GET) { + if(session.getUri().startsWith("/status")) { + + // Serve the current status data on Json. + session.getUri() match { + case "/status/config" => json(status.baseInfo()) + case "/status/modules" => json(status.moduleRegistry()) + case _ => NotFound + } + + } else { + // Serve resources from the status page folder. + val resource = if (session.getUri() == "/") "/index.html" else session.getUri() + val resourcePath = RootResourceDirectory + resource + val resourceStream = resourceLoader.getResourceAsStream(resourcePath) + + if (resourceStream == null) NotFound else { + NanoHTTPD.newChunkedResponse(StatusCode.OK, mimeType(resource), resourceStream) + } + } + + } else NotAllowed + } + + private def mimeType(resource: String): String = { + val ResourceExtensionRegex(resourceExtension) = resource + resourceExtension match { + case "css" => "text/css" + case "js" => "application/javascript" + case "ico" => "image/x-icon" + case "html" => "text/html" + case _ => "text/plain" + } + } + + private def json[T : JsonMarshalling](instance: T): Response = { + val builder = new java.lang.StringBuilder() + implicitly[JsonMarshalling[T]].toJson(instance, builder) + NanoHTTPD.newFixedLengthResponse(StatusCode.OK, "application/json", builder.toString()) + } + + private val NotAllowed = NanoHTTPD.newFixedLengthResponse( + StatusCode.METHOD_NOT_ALLOWED, + NanoHTTPD.MIME_PLAINTEXT, + "Only GET requests are allowed.") + + private val NotFound = NanoHTTPD.newFixedLengthResponse( + StatusCode.NOT_FOUND, + NanoHTTPD.MIME_PLAINTEXT, + "The requested resource was not found.") +}
\ No newline at end of file |