Create Plugin
This page explains how to create custom plugins for Colotok. Plugins allow you to extend Colotok's functionality in various ways, such as adding support for new logging destinations, implementing new formatting options, or integrating with other libraries and frameworks.
Plugin Architecture
Colotok has a flexible plugin architecture based on interfaces that define the contract between the core library and plugins. The main interfaces are:
Provider: The core interface that all logging providers must implement
ProviderConfig: The interface for provider configuration
AsyncProvider: An extension of Provider that adds support for asynchronous logging
Provider Interface
The Provider
interface is the foundation of Colotok's plugin system. It defines methods for writing logs to various destinations:
interface Provider {
// Write plain text logs
fun write(name: String, msg: String, level: Level)
// Write plain text logs with attributes
fun write(name: String, msg: String, level: Level, attr: Map<String, String>)
// Write structured logs
fun <T : LogStructure> write(name: String, msg: T, serializer: KSerializer<T>, level: Level)
// Write structured logs with attributes
fun <T : LogStructure> write(name: String, msg: T, serializer: KSerializer<T>, level: Level, attr: Map<String, String>)
}
At minimum, a provider needs to implement the second and fourth methods, as the first and third methods have default implementations that call the second and fourth methods respectively with an empty map.
ProviderConfig Interface
The ProviderConfig
interface defines the basic configuration options that all providers must support:
interface ProviderConfig {
// Minimum log level that the provider will process
var level: Level
// Formatter used to format log messages
var formatter: Formatter
}
Specific providers can extend this interface to add their own configuration options.
AsyncProvider Interface
The AsyncProvider
interface extends the Provider
interface to add support for asynchronous logging:
interface AsyncProvider: Provider {
// Async versions of the Provider methods
suspend fun writeAsync(name: String, msg: String, level: Level)
suspend fun writeAsync(name: String, msg: String, level: Level, attr: Map<String, String>)
suspend fun <T : LogStructure> writeAsync(name: String, msg: T, serializer: KSerializer<T>, level: Level)
suspend fun <T : LogStructure> writeAsync(name: String, msg: T, serializer: KSerializer<T>, level: Level, attr: Map<String, String>)
}
Similar to the Provider
interface, at minimum, an AsyncProvider
needs to implement the second and fourth methods.
Creating a Custom Provider
Let's walk through the process of creating a custom provider for Colotok. We'll create a provider that sends logs to Slack using webhooks.
Step 1: Define the Provider Configuration
First, define a configuration class for your provider by implementing the ProviderConfig
interface:
class SlackProviderConfig : ProviderConfig {
// Required by ProviderConfig
override var level: Level = LogLevel.DEBUG
override var formatter: Formatter = DetailTextFormatter
// Custom configuration options
var webhookUrl: String = ""
}
Step 2: Implement the Provider
Next, implement the Provider
interface:
class SlackProvider(config: SlackProviderConfig) : Provider {
// Convenience constructor that accepts a configuration lambda
constructor(config: SlackProviderConfig.() -> Unit): this(SlackProviderConfig().apply(config))
private val webhookUrl = config.webhookUrl
private val logLevel = config.level
private val formatter = config.formatter
// HTTP client for sending requests to Slack
private val client = HttpClient()
override fun write(
name: String,
msg: String,
level: Level,
attr: Map<String, String>
) {
// Skip if the log level is not enabled
if (level.isEnabledFor(logLevel).not()) {
return
}
// Format the log message
val formattedMessage = formatter.format(msg, level, attr)
// Send the log to Slack
runBlocking {
client.post(webhookUrl) {
contentType(ContentType.Application.Json)
setBody("""{"text": "${formattedMessage.escapeJson()}"}""")
}
}
}
override fun <T : LogStructure> write(
name: String,
msg: T,
serializer: KSerializer<T>,
level: Level,
attr: Map<String, String>
) {
// Skip if the log level is not enabled
if (level.isEnabledFor(logLevel).not()) {
return
}
// Format the structured log message
val formattedMessage = formatter.format(msg, serializer, level, attr)
// Send the log to Slack
runBlocking {
client.post(webhookUrl) {
contentType(ContentType.Application.Json)
setBody("""{"text": "${formattedMessage.escapeJson()}"}""")
}
}
}
// Helper function to escape JSON strings
private fun String.escapeJson(): String {
return this.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "\\r")
.replace("\t", "\\t")
}
}
Step 3: Add Asynchronous Support (Optional)
If you want to support asynchronous logging, implement the AsyncProvider
interface:
class SlackProvider(config: SlackProviderConfig) : AsyncProvider {
// ... same as before ...
override suspend fun writeAsync(
name: String,
msg: String,
level: Level,
attr: Map<String, String>
) {
// Skip if the log level is not enabled
if (level.isEnabledFor(logLevel).not()) {
return
}
// Format the log message
val formattedMessage = formatter.format(msg, level, attr)
// Send the log to Slack asynchronously
client.post(webhookUrl) {
contentType(ContentType.Application.Json)
setBody("""{"text": "${formattedMessage.escapeJson()}"}""")
}
}
override suspend fun <T : LogStructure> writeAsync(
name: String,
msg: T,
serializer: KSerializer<T>,
level: Level,
attr: Map<String, String>
) {
// Skip if the log level is not enabled
if (level.isEnabledFor(logLevel).not()) {
return
}
// Format the structured log message
val formattedMessage = formatter.format(msg, serializer, level, attr)
// Send the log to Slack asynchronously
client.post(webhookUrl) {
contentType(ContentType.Application.Json)
setBody("""{"text": "${formattedMessage.escapeJson()}"}""")
}
}
// For synchronous methods, delegate to async methods
override fun write(
name: String,
msg: String,
level: Level,
attr: Map<String, String>
) {
runBlocking {
writeAsync(name, msg, level, attr)
}
}
override fun <T : LogStructure> write(
name: String,
msg: T,
serializer: KSerializer<T>,
level: Level,
attr: Map<String, String>
) {
runBlocking {
writeAsync(name, msg, serializer, level, attr)
}
}
}
Step 4: Use Your Custom Provider
Now you can use your custom provider with Colotok:
val logger = ColotokLoggerFactory()
.addProvider(SlackProvider {
webhookUrl = "https://hooks.slack.com/services/your/webhook/url"
level = LogLevel.WARN // Only send WARN and ERROR logs to Slack
formatter = SimpleTextFormatter
})
.getLogger()
// Use the logger as normal
logger.info("This won't be sent to Slack")
logger.warn("This will be sent to Slack")
logger.error("This will also be sent to Slack")
Best Practices
Here are some best practices to follow when creating custom plugins for Colotok:
1. Respect Log Levels
Always check the log level before processing a log message:
if (level.isEnabledFor(logLevel).not()) {
return
}
This ensures that your provider only processes logs that meet the configured minimum level.
2. Handle Errors Gracefully
Logging should never cause your application to crash. Always wrap external calls in try-catch blocks:
runCatching {
// External call that might fail
}.getOrElse { exception ->
// Handle the exception, maybe log it to a fallback destination
println("Failed to send log: ${exception.message}")
}
3. Buffer Logs When Appropriate
If your provider sends logs to an external service, consider buffering logs to reduce the number of network requests:
private val buffer = mutableListOf<LogEntry>()
private val bufferSize = 50
private val mutex = Mutex()
override suspend fun writeAsync(
name: String,
msg: String,
level: Level,
attr: Map<String, String>
) {
if (level.isEnabledFor(logLevel).not()) {
return
}
val shouldSendLogs = mutex.withLock {
buffer.add(LogEntry(name, formatter.format(msg, level, attr), System.currentTimeMillis()))
buffer.size >= bufferSize
}
if (shouldSendLogs) {
sendLogsToExternalService()
}
}
private suspend fun sendLogsToExternalService() {
mutex.withLock {
if (buffer.isEmpty()) return
// Send the buffered logs
client.post(serviceUrl) {
contentType(ContentType.Application.Json)
setBody(buffer)
}
// Clear the buffer
buffer.clear()
}
}
// Add a flush method to send any buffered logs
suspend fun flush() {
sendLogsToExternalService()
}
4. Make Configuration Flexible
Provide sensible defaults but allow users to customize your provider:
class MyProviderConfig : ProviderConfig {
override var level: Level = LogLevel.INFO // Sensible default
override var formatter: Formatter = SimpleTextFormatter // Sensible default
var bufferSize: Int = 50 // Sensible default
var retryCount: Int = 3 // Sensible default
var timeout: Long = 5000 // Sensible default
}
5. Document Your Provider
Add KDoc comments to your provider and configuration classes to help users understand how to use them:
/**
* A provider that sends logs to MyService.
*
* This provider buffers logs and sends them in batches to reduce network traffic.
* It also supports retrying failed requests.
*
* @property config The configuration for this provider
*/
class MyProvider(config: MyProviderConfig) : Provider {
// ...
}
Testing Your Provider
It's important to test your provider to ensure it works correctly. Here's a simple test for our SlackProvider:
class SlackProviderTest {
@Test
fun `test slack provider sends logs`() {
// Create a mock HTTP client
val mockClient = MockHttpClient { request ->
// Verify the request
assertEquals("https://hooks.slack.com/services/test", request.url.toString())
assertEquals(ContentType.Application.Json, request.contentType())
// Return a success response
MockHttpResponse(HttpStatusCode.OK, "ok")
}
// Create the provider with the mock client
val provider = SlackProvider {
webhookUrl = "https://hooks.slack.com/services/test"
level = LogLevel.INFO
}.apply {
client = mockClient
}
// Test the provider
provider.write("test", "Test message", LogLevel.INFO, mapOf())
// Verify that the mock client was called
assertEquals(1, mockClient.requestCount)
}
}
Packaging Your Plugin
If you want to share your plugin with others, you should package it as a separate library. Here's a basic structure for a Colotok plugin project:
my-colotok-plugin/
├── build.gradle.kts
├── src/
│ ├── main/
│ │ └── kotlin/
│ │ └── com/
│ │ └── example/
│ │ └── colotok/
│ │ └── plugin/
│ │ ├── MyProvider.kt
│ │ └── MyProviderConfig.kt
│ └── test/
│ └── kotlin/
│ └── com/
│ └── example/
│ └── colotok/
│ └── plugin/
│ └── MyProviderTest.kt
└── README.md
Your build.gradle.kts
file should include Colotok as a dependency:
plugins {
kotlin("jvm") version "2.1.10"
kotlin("plugin.serialization") version "2.1.10"
`maven-publish`
}
group = "com.example"
version = "0.1.0"
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.milkcocoa0902:colotok:0.3.3")
// Add any other dependencies your plugin needs
testImplementation(kotlin("test"))
}
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}
Conclusion
Creating custom plugins for Colotok is a powerful way to extend its functionality to meet your specific needs. By implementing the Provider
interface and following the best practices outlined in this guide, you can create robust and flexible plugins that integrate Colotok with any logging destination or service.
Remember to check the Official Plugin page for examples of plugins that are officially supported by Colotok.
Last modified: 07 July 2025