aboutsummaryrefslogtreecommitdiff
path: root/kamon-core/src/main/scala/kamon
diff options
context:
space:
mode:
Diffstat (limited to 'kamon-core/src/main/scala/kamon')
-rw-r--r--kamon-core/src/main/scala/kamon/Kamon.scala24
-rw-r--r--kamon-core/src/main/scala/kamon/ModuleLoading.scala2
-rw-r--r--kamon-core/src/main/scala/kamon/StatusPage.scala61
-rw-r--r--kamon-core/src/main/scala/kamon/module/Module.scala60
-rw-r--r--kamon-core/src/main/scala/kamon/status/JsonMarshalling.scala65
-rw-r--r--kamon-core/src/main/scala/kamon/status/Status.scala37
-rw-r--r--kamon-core/src/main/scala/kamon/status/StatusPageServer.scala65
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