From f35851d339d1415e8e3d66d14f1400c0946a0fa1 Mon Sep 17 00:00:00 2001 From: "gowthaman.b" Date: Sat, 11 Nov 2023 16:13:59 +0530 Subject: [PATCH] tighten the api --- .gitignore | 4 +- api.http | 13 +- app-sample.properties | 4 +- build.gradle.kts | 4 +- .../kotlin/com/restapi/AppAccessManager.kt | 8 +- src/main/kotlin/com/restapi/Main.kt | 32 ++++- .../kotlin/com/restapi/config/AppConfig.kt | 3 + .../com/restapi/controllers/Entities.kt | 1 + src/main/kotlin/com/restapi/domain/db.kt | 3 + src/main/kotlin/com/restapi/domain/models.kt | 46 +++++-- .../dbmigration/1.0.1__initial_data.sql | 2 + .../resources/dbmigration/1.0__initial.sql | 115 +++++++++--------- .../dbmigration/model/1.0__initial.model.xml | 110 ++++++++--------- src/main/resources/extra-ddl.xml | 24 ++++ src/main/resources/logback.xml | 21 ++++ 15 files changed, 247 insertions(+), 143 deletions(-) create mode 100644 src/main/resources/dbmigration/1.0.1__initial_data.sql create mode 100644 src/main/resources/extra-ddl.xml create mode 100644 src/main/resources/logback.xml diff --git a/.gitignore b/.gitignore index b5991bb..5888071 100644 --- a/.gitignore +++ b/.gitignore @@ -40,4 +40,6 @@ bin/ ### Mac OS ### .DS_Store -application.yaml \ No newline at end of file +application.yaml +initial-data.sql +*.env.json \ No newline at end of file diff --git a/api.http b/api.http index d714477..9b38958 100644 --- a/api.http +++ b/api.http @@ -1,19 +1,19 @@ ### create row POST http://localhost:9001/api/vehicle Content-Type: application/json -Authorization: set-auth-token +Authorization: {{auth-token}} { "data": { - "number": "TN36BA5009" + "number": "KA01MU0556" }, - "uniqueIdentifier": "TN36BA5009" + "uniqueIdentifier": "KA01MU0556" } ### create row, with autogenerated identifier POST http://localhost:9001/api/log Content-Type: application/json -Authorization: set-auth-token +Authorization: {{auth-token}} { "data": { @@ -23,7 +23,8 @@ Authorization: set-auth-token } ### get row -GET http://localhost:9001/api/vehicle/KA03HD6064 +GET http://localhost:9001/api/vehicle/TN36BA5009 +Authorization: Bearer {{auth-token}} ### query row POST http://localhost:9001/api/vehicle/query @@ -40,7 +41,7 @@ Authorization: set-auth-token ### update field PATCH http://localhost:9001/api/vehicle/KA03HD6064 Content-Type: application/json -Authorization: set-auth-token +Authorization: {{auth-token}} { "key": "ownerName", diff --git a/app-sample.properties b/app-sample.properties index fc24c72..48a2cd1 100644 --- a/app-sample.properties +++ b/app-sample.properties @@ -5,8 +5,10 @@ app.db.user=postgres app.db.pass=postgres app.db.url=jdbc:postgresql://192.168.64.6/modules_app app.db.run_migration=true +app.db.seed_sql=initial-data.sql app.iam.url=https://auth.compegence.com app.iam.realm=forewarn-dev app.iam.client=forewarn app.iam.client_redirect_uri=http://localhost:9001/auth/code -app.cache.redis_uri=redis://127.0.0.1:6379/0 \ No newline at end of file +app.cache.redis_uri=redis://127.0.0.1:6379/0 +app.scripts.path=/tmp \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 8ed7383..03dfed3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -25,9 +25,9 @@ dependencies { implementation("io.ebean:ebean-ddl-generator:13.23.2") implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.15.+") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.+") - + implementation("ch.qos.logback:logback-core:1.4.11") + implementation("ch.qos.logback:logback-classic:1.4.11") implementation("org.bitbucket.b_c:jose4j:0.9.3") - implementation("org.slf4j:slf4j-simple:2.0.7") implementation("redis.clients:jedis:5.0.2") implementation("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.9.0") api ("net.cactusthorn.config:config-core:0.81") diff --git a/src/main/kotlin/com/restapi/AppAccessManager.kt b/src/main/kotlin/com/restapi/AppAccessManager.kt index f1b7498..813fdc6 100644 --- a/src/main/kotlin/com/restapi/AppAccessManager.kt +++ b/src/main/kotlin/com/restapi/AppAccessManager.kt @@ -26,7 +26,6 @@ class AppAccessManager : AccessManager { override fun manage(handler: Handler, ctx: Context, routeRoles: Set) { val pathParamMap = ctx.pathParamMap() - logger.warn("access {}, {}", pathParamMap, routeRoles) val regex = Regex("^[a-zA-Z0-9\\-_\\.]+$") if (pathParamMap.values.count { !regex.matches(it) } > 0) { @@ -35,15 +34,16 @@ class AppAccessManager : AccessManager { val entity = pathParamMap["entity"] val action = pathParamMap["action"] - val allowedRoles = routeRoles.map { it as Role }.flatMap { - when (it) { + val allowedRoles = routeRoles.map { it as Roles }.flatMap { it.roles.toList() }.flatMap { role -> + when (role) { Role.DbOps -> listOf("ROLE_DB_OPS") Role.Entity -> loadEntityActionRole(entity, action) - is Role.Standard -> listOf("ROLE_${entity}_${it.action}") + is Role.Standard -> role.action.toList().map { "ROLE_${entity}_${it}" } }.map(String::uppercase) } val isAllowed = currentRoles().count { allowedRoles.contains(it) } > 0 + logger.warn("entity - $entity, action $action, userroles = ${currentRoles()}, allowed = $allowedRoles, isAllowed? $isAllowed, enforce? ${appConfig.enforceRoleRestriction()}") if (isAllowed || !appConfig.enforceRoleRestriction() || allowedRoles.isEmpty()) { //if role is allowed, or enforcement is turned off or no roles are explicitly allowed handler.handle(ctx) diff --git a/src/main/kotlin/com/restapi/Main.kt b/src/main/kotlin/com/restapi/Main.kt index fd63bdd..5a4a5fb 100644 --- a/src/main/kotlin/com/restapi/Main.kt +++ b/src/main/kotlin/com/restapi/Main.kt @@ -11,6 +11,7 @@ import com.restapi.domain.DataNotFoundException import com.restapi.domain.Session.objectMapper import com.restapi.domain.Session.redis import com.restapi.domain.Session.setAuthorizedUser +import io.ebean.DataIntegrityException import io.ebean.DuplicateKeyException import io.javalin.Javalin import io.javalin.apibuilder.ApiBuilder.* @@ -19,6 +20,7 @@ import io.javalin.http.util.NaiveRateLimit import io.javalin.http.util.RateLimitUtil import io.javalin.json.JavalinJackson import io.javalin.security.RouteRole +import org.jose4j.jwt.consumer.InvalidJwtException import org.slf4j.LoggerFactory import java.net.URI import java.net.URLEncoder @@ -108,8 +110,6 @@ fun main(args: Array) { ?.replace("Bearer: ", "") ?.trim() ?: throw UnauthorizedResponse() - logger.warn("authToken = $authToken") - setAuthorizedUser(parseAuthToken(authToken = authToken)) } @@ -139,34 +139,54 @@ fun main(args: Array) { } - .exception(DuplicateKeyException::class.java) { _, ctx -> + .exception(DuplicateKeyException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) ctx.json( mapOf( "error" to "Duplicate Data" ) ).status(HttpStatus.CONFLICT) } - .exception(DataNotFoundException::class.java) { _, ctx -> + .exception(DataIntegrityException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) + ctx.json( + mapOf( + "error" to "References Missing" + ) + ).status(HttpStatus.EXPECTATION_FAILED) + } + .exception(DataNotFoundException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) ctx.json( mapOf( "error" to "Data Not Found" ) ).status(HttpStatus.NOT_FOUND) } - .exception(IllegalArgumentException::class.java) { _, ctx -> + .exception(IllegalArgumentException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) ctx.json( mapOf( "error" to "Incorrect Data" ) ).status(HttpStatus.BAD_REQUEST) } - .exception(JsonMappingException::class.java) { _, ctx -> + .exception(JsonMappingException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) ctx.json( mapOf( "error" to "Incorrect Data" ) ).status(HttpStatus.BAD_REQUEST) } + .exception(InvalidJwtException::class.java) { e, ctx -> + logger.warn("while processing ${ctx.path()}, exception ${e.message}", e) + ctx.json( + mapOf( + "error" to "Login required" + ) + ).status(HttpStatus.UNAUTHORIZED) + } .start(appConfig.portNumber()) } diff --git a/src/main/kotlin/com/restapi/config/AppConfig.kt b/src/main/kotlin/com/restapi/config/AppConfig.kt index c1a90e1..9e09776 100644 --- a/src/main/kotlin/com/restapi/config/AppConfig.kt +++ b/src/main/kotlin/com/restapi/config/AppConfig.kt @@ -42,6 +42,9 @@ interface AppConfig { @Key("app.db.run_migration") fun dbRunMigration(): Boolean + @Key("app.db.seed_sql") + fun seedSqlFile(): Optional + @Key("app.iam.url") fun iamUrl(): String diff --git a/src/main/kotlin/com/restapi/controllers/Entities.kt b/src/main/kotlin/com/restapi/controllers/Entities.kt index 86f5c07..f699ef6 100644 --- a/src/main/kotlin/com/restapi/controllers/Entities.kt +++ b/src/main/kotlin/com/restapi/controllers/Entities.kt @@ -140,6 +140,7 @@ object Entities { if (this.uniqueIdentifier.isEmpty()) { this.uniqueIdentifier = Session.nextUniqId(entity) } + this.approvalStatus = ApprovalStatus.APPROVED } database.save( diff --git a/src/main/kotlin/com/restapi/domain/db.kt b/src/main/kotlin/com/restapi/domain/db.kt index 50441d1..9d5f5d8 100644 --- a/src/main/kotlin/com/restapi/domain/db.kt +++ b/src/main/kotlin/com/restapi/domain/db.kt @@ -32,6 +32,9 @@ object Session { setProperty("datasource.db.password", appConfig.dbPass()) setProperty("datasource.db.url", appConfig.dbUrl()) setProperty("ebean.migration.run", appConfig.dbRunMigration().toString()) + if(appConfig.seedSqlFile().isPresent){ + setProperty("ebean.ddl.seedSql", appConfig.seedSqlFile().get()) + } }) tenantMode = TenantMode.PARTITION currentTenantProvider = CurrentTenantProvider { currentUser.get().tenant } diff --git a/src/main/kotlin/com/restapi/domain/models.kt b/src/main/kotlin/com/restapi/domain/models.kt index b86928c..e4f7fbc 100644 --- a/src/main/kotlin/com/restapi/domain/models.kt +++ b/src/main/kotlin/com/restapi/domain/models.kt @@ -6,6 +6,7 @@ import com.fasterxml.jackson.databind.JsonDeserializer import com.fasterxml.jackson.databind.annotation.JsonDeserialize import io.ebean.Model import io.ebean.annotation.DbArray +import io.ebean.annotation.DbDefault import io.ebean.annotation.DbJsonB import io.ebean.annotation.Index import io.ebean.annotation.Platform @@ -31,19 +32,22 @@ abstract class BaseModel : Model() { var sysPk: Long = 0 @SoftDelete + @DbDefault("false") var deleted: Boolean = false @Version + @DbDefault("1") var version: Int = 0 @WhenCreated + @DbDefault("now()") var createdAt: LocalDateTime = LocalDateTime.now() @WhenModified + @DbDefault("now()") var modifiedAt: LocalDateTime? = null - @TenantId - var tenantId: String = "" + @WhoCreated var createdBy: String = "" @@ -55,22 +59,34 @@ abstract class BaseModel : Model() { var deletedBy: String? = null + @DbDefault("0") var currentApprovalLevel: Int = 0 - + @DbDefault("0") var requiredApprovalLevels: Int = 0 @Enumerated(EnumType.STRING) - var approvalStatus: ApprovalStatus = ApprovalStatus.PENDING + @DbDefault("APPROVED") + var approvalStatus: ApprovalStatus = ApprovalStatus.APPROVED @DbArray + @DbDefault("{}") var tags: MutableList = arrayListOf() @DbJsonB + @DbDefault("[]") var comments: MutableList = arrayListOf() } + +@MappedSuperclass +abstract class BaseTenantModel : BaseModel() { + @TenantId + var tenantId: String = "" +} + @Entity open class TenantModel : BaseModel() { + @Index(unique = true) var name: String = "" var domain: String = "" var mobile: List = emptyList() @@ -86,7 +102,7 @@ enum class AuditType { @Entity @Index(columnNames = ["audit_type", "entity", "unique_identifier", "tenant_id", "created_by"]) -open class AuditLog : BaseModel() { +open class AuditLog : BaseTenantModel() { @Enumerated(EnumType.STRING) var auditType: AuditType = AuditType.CREATE @@ -94,48 +110,54 @@ open class AuditLog : BaseModel() { var uniqueIdentifier: String = "" @DbJsonB - @Index(definition = "create index audit_log_values_idx on audit_log using GIN (data) ", platforms = [Platform.POSTGRES]) + @Index(definition = "create index audit_log_values_idx on audit_log using GIN (data)", platforms = [Platform.POSTGRES]) var data: Map = hashMapOf() @DbJsonB - @Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes) ", platforms = [Platform.POSTGRES]) + @Index(definition = "create index audit_log_changes_idx on audit_log using GIN (changes)", platforms = [Platform.POSTGRES]) var changes: Map = hashMapOf() } @Entity -open class EntityModel : BaseModel() { +open class EntityModel : BaseTenantModel() { @Index(unique = true) @JsonDeserialize(using = SafeStringDeserializer::class) var name: String = "" //a kts script that will return true/false along with errors before saving - var preSaveScript: String = "" + var preSaveScript: String? = "" //a kts script that will do something ... returns void - var postSaveScript: String = "" + var postSaveScript: String? = "" //this will create extra actions/roles in keycloak //the default actions are create, update, view, delete @DbArray + @DbDefault("{}") var actions: List = emptyList() //allow only these fields, if this is empty, then all fields are allowed @DbArray + @DbDefault("{}") var allowedFields: List = emptyList() //enforce field types, if this is present, only fields that are present is validated @DbJsonB + @DbDefault("{}") var allowedFieldTypes: Map = hashMapOf() //when an entity is saved/updated audit logs will be populated, when this is empty, all fields are logged @DbArray + @DbDefault("{}") var auditLogFields: List = emptyList() @DbJsonB + @DbDefault("{}") var preferences: MutableMap = hashMapOf() //if '0' then its auto saved, no approval steps are required, for further steps, //a user needs to have ROLE_ENTITY_APPROVE_LEVEL1, ROLE_ENTITY_APPROVE_LEVEL2 roles for further approvals + @DbDefault("0") var approvalLevels: Int = 0 } @@ -148,7 +170,7 @@ enum class JobType { } @Entity -open class JobModel : BaseModel() { +open class JobModel : BaseTenantModel() { @Index(unique = true) var jobName: String = "" @@ -168,7 +190,7 @@ open class JobModel : BaseModel() { @Entity @Index(unique = true, name = "entity_unique_id", columnNames = ["entity_name", "unique_identifier", "tenant_id"]) -open class DataModel : BaseModel() { +open class DataModel : BaseTenantModel() { @JsonDeserialize(using = SafeStringDeserializer::class) var uniqueIdentifier: String = "" diff --git a/src/main/resources/dbmigration/1.0.1__initial_data.sql b/src/main/resources/dbmigration/1.0.1__initial_data.sql new file mode 100644 index 0000000..4204d8f --- /dev/null +++ b/src/main/resources/dbmigration/1.0.1__initial_data.sql @@ -0,0 +1,2 @@ +insert into tenant_model(name, domain, created_by, modified_by) values ('compegence', 'https://www.compegence.com', 'system', 'system'); +insert into entity_model(name, tenant_id, created_by, modified_by) values ('vehicle', 'compegence', 'system', 'system'); \ No newline at end of file diff --git a/src/main/resources/dbmigration/1.0__initial.sql b/src/main/resources/dbmigration/1.0__initial.sql index e43668f..089c80f 100644 --- a/src/main/resources/dbmigration/1.0__initial.sql +++ b/src/main/resources/dbmigration/1.0__initial.sql @@ -2,17 +2,17 @@ create table audit_log ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, - current_approval_level integer not null, - required_approval_levels integer not null, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, deleted boolean default false not null, - version integer not null, - created_at timestamp not null, - modified_at timestamp not null, - tenant_id varchar(255) not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, deleted_by varchar(255), - approval_status varchar(8) not null, - tags varchar[] not null, - comments jsonb not null, + approval_status varchar(8) default 'APPROVED' not null, + tags text[] default '{}'::text[] not null, + comments jsonb default '[]'::jsonb not null, + tenant_id varchar(255) not null, audit_type varchar(7) not null, entity varchar(255) not null, unique_identifier varchar(255) not null, @@ -28,17 +28,17 @@ create table audit_log ( create table data_model ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, - current_approval_level integer not null, - required_approval_levels integer not null, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, deleted boolean default false not null, - version integer not null, - created_at timestamp not null, - modified_at timestamp not null, - tenant_id varchar(255) not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, deleted_by varchar(255), - approval_status varchar(8) not null, - tags varchar[] not null, - comments jsonb not null, + approval_status varchar(8) default 'APPROVED' not null, + tags text[] default '{}'::text[] not null, + comments jsonb default '[]'::jsonb not null, + tenant_id varchar(255) not null, unique_identifier varchar(255) not null, entity_name varchar(255) not null, data jsonb not null, @@ -52,26 +52,26 @@ create table data_model ( create table entity_model ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, - current_approval_level integer not null, - required_approval_levels integer not null, - approval_levels integer not null, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, + approval_levels integer default 0 not null, deleted boolean default false not null, - version integer not null, - created_at timestamp not null, - modified_at timestamp not null, - tenant_id varchar(255) not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, deleted_by varchar(255), - approval_status varchar(8) not null, - tags varchar[] not null, - comments jsonb not null, + approval_status varchar(8) default 'APPROVED' not null, + tags text[] default '{}'::text[] not null, + comments jsonb default '[]'::jsonb not null, + tenant_id varchar(255) not null, name varchar(255) not null, - pre_save_script varchar(255) not null, - post_save_script varchar(255) not null, - actions varchar[] not null, - allowed_fields varchar[] not null, - allowed_field_types jsonb not null, - audit_log_fields varchar[] not null, - preferences jsonb not null, + pre_save_script varchar(255), + post_save_script varchar(255), + actions text[] default '{}'::text[] not null, + allowed_fields text[] default '{}'::text[] not null, + allowed_field_types jsonb default '{}'::jsonb not null, + audit_log_fields text[] default '{}'::text[] not null, + preferences jsonb default '{}'::jsonb not null, created_by varchar(255) not null, modified_by varchar(255) not null, constraint ck_entity_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), @@ -82,17 +82,17 @@ create table entity_model ( create table job_model ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, - current_approval_level integer not null, - required_approval_levels integer not null, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, deleted boolean default false not null, - version integer not null, - created_at timestamp not null, - modified_at timestamp not null, - tenant_id varchar(255) not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, deleted_by varchar(255), - approval_status varchar(8) not null, - tags varchar[] not null, - comments jsonb not null, + approval_status varchar(8) default 'APPROVED' not null, + tags text[] default '{}'::text[] not null, + comments jsonb default '[]'::jsonb not null, + tenant_id varchar(255) not null, job_name varchar(255) not null, job_type varchar(6) not null, job_path varchar(255) not null, @@ -111,28 +111,31 @@ create table job_model ( create table tenant_model ( sys_pk bigint generated by default as identity not null, deleted_on timestamp, - current_approval_level integer not null, - required_approval_levels integer not null, + current_approval_level integer default 0 not null, + required_approval_levels integer default 0 not null, deleted boolean default false not null, - version integer not null, - created_at timestamp not null, - modified_at timestamp not null, - tenant_id varchar(255) not null, + version integer default 1 not null, + created_at timestamp default 'now()' not null, + modified_at timestamp default 'now()' not null, deleted_by varchar(255), - approval_status varchar(8) not null, - tags varchar[] not null, - comments jsonb not null, + approval_status varchar(8) default 'APPROVED' not null, + tags text[] default '{}'::text[] not null, + comments jsonb default '[]'::jsonb not null, name varchar(255) not null, domain varchar(255) not null, - preferences jsonb not null, + preferences jsonb default '{}'::jsonb not null, created_by varchar(255) not null, modified_by varchar(255) not null, constraint ck_tenant_model_approval_status check ( approval_status in ('PENDING','APPROVED','REJECTED')), + constraint uq_tenant_model_name unique (name), constraint pk_tenant_model primary key (sys_pk) ); -- foreign keys and indices create index if not exists ix_audit_log_audit_type_entity_unique_identifier_tenant_i_1 on audit_log (audit_type,entity,unique_identifier,tenant_id,created_by); -create index audit_log_values_idx on audit_log using GIN (data) ; -create index audit_log_changes_idx on audit_log using GIN (changes) ; +create index audit_log_values_idx on audit_log using GIN (data); +create index audit_log_changes_idx on audit_log using GIN (changes); create index data_jsonb_idx on data_model using GIN (data) ; + +ALTER TABLE data_model ADD FOREIGN KEY(tenant_id) REFERENCES tenant_model(name); +ALTER TABLE data_model ADD FOREIGN KEY(entity_name) REFERENCES entity_model(name); \ No newline at end of file diff --git a/src/main/resources/dbmigration/model/1.0__initial.model.xml b/src/main/resources/dbmigration/model/1.0__initial.model.xml index c92a04a..c9895a1 100644 --- a/src/main/resources/dbmigration/model/1.0__initial.model.xml +++ b/src/main/resources/dbmigration/model/1.0__initial.model.xml @@ -3,84 +3,84 @@ - - - - - - + + + + + + - - - + + + - - - - - - + + + + + + - - - + + + - - - - - - + + + + + + - - - - - - - - + + + + + + + + - - - + + + - - - - - - + + + + + + @@ -88,36 +88,36 @@ - - - + + + - - - - - - + + + + + - - - + + + + - - + + \ No newline at end of file diff --git a/src/main/resources/extra-ddl.xml b/src/main/resources/extra-ddl.xml new file mode 100644 index 0000000..1ab8ca3 --- /dev/null +++ b/src/main/resources/extra-ddl.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000..20080db --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,21 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + + \ No newline at end of file