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

rolling log #691

Merged
merged 22 commits into from
Mar 28, 2023
Merged

rolling log #691

merged 22 commits into from
Mar 28, 2023

Conversation

SHSongs
Copy link
Contributor

@SHSongs SHSongs commented Mar 16, 2023

I use Slf4jBridge in my project, but zio-logging does not include rolling log function,
Therefore, I have created a simple rolling log function.
could you review the PR?

@SHSongs SHSongs requested a review from a team as a code owner March 16, 2023 12:58
@justcoon
Copy link
Contributor

justcoon commented Mar 16, 2023

hi @SHSongs

thank you for contribution, that is missing feature which will be nice to add, I appreciate yours contribution

however there are few things to improve:

  1. instead of simple FileLoggerConfig.rolling boolean flag, we need to be more open, if you take https://logback.qos.ch/manual/appenders.html#onRollingPolicies as inspiration,
    we will need to add something like
object FileLoggerConfig {
  sealed trait FileRollingPolicy
  
  object FileRollingPolicy {
     case class TimeBasedRollingPolicy( /* some attributes related to this policy if needed */) extends FileRollingPolicy 
     // case class FixedWindowRollingPolicy() extends FileRollingPolicy   // can be implemented later ...
  }
}

then

final case class FileLoggerConfig(
  ...
  bufferedIOSize: Option[Int] = None
  rollingPolicy: Option[FileRollingPolicy] = None
)

then this needs to be added to FileLoggerConfig.config ...

  1. FileWriter needs to work with this policy, if there is new file based on policy OutputStreamWriter needs to be opened just once, also you need to close previous one if there is new one created based on changes in relation to rolling policy

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 19, 2023

I think I fixed all the code you reviewed. Thanks for your time and effort reviewed my code.

case FileRollingPolicy.TimeBasedRollingPolicy =>
val newPath = makeDatePath()
if (newPath != currentDestination) {
println("새로운 파일 생성")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i assume this is for "Debug" purposes and it will be removed ...

bufferedIOSize match {
case Some(bufferSize) => new BufferedWriter(output, bufferSize)
case None => output
}
}
private var writer: Writer = rolling match {
Copy link
Contributor

@justcoon justcoon Mar 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in case of rollingPolicy, i think some concurrent constructs will have to be used as at same time 2 threads/fibers may try to log, and we do not want to create new writer twice ...

in addition to that, personally i would do something like

private[logging] class FileWriter(
  destination: Path,
  charset: Charset,
  autoFlushBatchSize: Int,
  bufferedIOSize: Option[Int],
  rolling: Option[FileLoggerConfig.FileRollingPolicy]
) extends Writer {
  private val writerProvider: WriterProvider = rolling match {
  ...
  }

  final def writeln(line: String): Unit = {
    val writer  = writerProvider.writer
    writer.write(line)
    writer.write(System.lineSeparator)

    entriesWritten += 1

    if (entriesWritten % autoFlushBatchSize == 0)
      writer.flush()
  }

  ...


}



private[logging] sealed trait WriterProvider {
  def writer: Writer
}

private[logging] object WriterProvider {

  final case class SimpleWriterProvider(
    destination: Path,
    charset: Charset,
    bufferedIOSize: Option[Int]
  ) extends WriterProvider {
    override val writer: Writer = {
      val output = new OutputStreamWriter(new FileOutputStream(destination.toFile, true), charset)
      bufferedIOSize match {
        case Some(bufferSize) => new BufferedWriter(output, bufferSize)
        case None             => output
      }
    }
  }


  final case class TimeBasedRollingWriterProvider(
                                         destination: Path,
                                         charset: Charset,
                                         bufferedIOSize: Option[Int]
                                       ) extends WriterProvider {
    override def writer: Writer = ???
  }
}

with such design you can isolate underlying writer problem, and it should be easier to add next policy later ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rewrote. Could you check this commit? 15d1768

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok, now i think it is better, just some other problems are still remaining

in case of rollingPolicy, i think some concurrent constructs will have to be used as at same time 2 threads/fibers may try to log, and we do not want to create new writer twice ...

#691 (comment)

policy match {
case FileRollingPolicy.TimeBasedRollingPolicy =>
val newPath = makeDatePath()
if (newPath != currentDestination) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we will need more optimal comparison probably just based on currentDateTime,
as makeDatePath is relatively complex, to do it with every log line

we need to have logging as fast as possible, we need to avoid unnecessary memory allocations and computations

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cb16d15
rewrote using currentDateTime. Could you check this commit?


final def close(): Unit = writerProvider.writer.close()

private[logging] sealed trait WriterProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

now WriterProvider is inside FileWriter

i think you can move it out, so also it can be tested separately

Copy link
Contributor Author

@SHSongs SHSongs Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote, Could you check this commit? 5dd2e73

@justcoon
Copy link
Contributor

also documentation will need to be updated https://github.com/zio/zio-logging/blob/master/docs/file-logger.md

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 20, 2023

i want to TestClock but not work. I think this part is the cause

so I used ZIO.logDebug("abcd").repeatWhileZIO(_ => ZIO.succeed(true).delay(100.millisecond)) in testing.

if this PR is close. Could I change java time to ZIO time?

@justcoon
Copy link
Contributor

@SHSongs

zio.ZLogger in is not effectful

that is one of reason why in those underlying impl. of loggers we are not using zio.* (Console, Clock ...) if it is not really needed

you can create custom format using zio.Unsafe to do some testing

  def timestamp(formatter: => DateTimeFormatter): LogFormat =
    text {
      val d = zio.Unsafe.unsafe { implicit u =>
        Runtime.default.unsafe.run(zio.Clock.instant).getOrThrow()
      }
      formatter.format(d.atZone(ZoneId.systemDefault()))
    }

but i think in LogFormat.timestamp implementation we do not want to change it now, as with zio.Unsafe it is more expensive operation then ZonedDateTime.now(), and logging should have minimum impact on performance of system

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 21, 2023

also documentation will need to be updated https://github.com/zio/zio-logging/blob/master/docs/file-logger.md

I updated the documentation. let me know if there is any missing information or anything that needs to be added.
90edf38


val spec: Spec[Environment, Any] = suite("WriterProvider")(
test("Make path include date") {
val destination = FileSystems.getDefault.getPath("/tmp/file_app.log")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add also test for paths like:

  • /tmp/file_app
  • /tmp/file.app.log
  • /tmp/file.app.out.log

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added test case

if (newTime != timeInUse) {
currentWriter.close()
currentWriter = makeWriter(makePath(destination, timeInUse, formatter), charset, bufferedIOSize)
timeInUse = newTime
Copy link
Contributor

@justcoon justcoon Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as i mentioned before, i think this implementation is not thread safe, some concurrency patterns should be used so only one caller will create new writer and close current one if it is needed

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do I make it like the code below? Also, could you provide an example code for concurrent patterns?

    import java.util.concurrent.locks.ReentrantLock
    private val lock: ReentrantLock = new ReentrantLock()

    override def writer: Writer = {
      val newTime = makeNewTime
      if (newTime != timeInUse) {
        lock.lock()
        try {
          currentWriter.close()
          currentWriter = makeWriter(makePath(destination, timeInUse), charset, bufferedIOSize)
          timeInUse = newTime
        } finally {
          lock.unlock()
        }
      }
      currentWriter
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could work

however i think time should be checked also under the lock

  override def writer: Writer = {
    val newTime = makeNewTime
    if (newTime != timeInUse) {
      lock.lock()
      try {
         if (newTime != timeInUse) {
            currentWriter.close() 
            currentWriter = makeWriter(makePath(destination, newTime), charset, bufferedIOSize) 
            timeInUse = newTime
        }
      } finally {
        lock.unlock()
      }
    } 
     currentWriter
  }

or just

 override def writer: Writer = {
    val newTime = makeNewTime
    
      lock.lock()
      try {
         if (newTime != timeInUse) {
            currentWriter.close() 
            currentWriter = makeWriter(makePath(destination, newTime), charset, bufferedIOSize) 
            timeInUse = newTime
        }
      } finally {
        lock.unlock()
      }
    
     currentWriter
  }

i think that first version should be less locking, even if it is probably less correct

format = LogFormat.default,
filter = LogFilter.logLevel(LogLevel.Debug),
rollingPolicy = Some(FileRollingPolicy.TimeBasedRollingPolicy)
)
Copy link
Contributor

@justcoon justcoon Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this example now do not match with https://github.com/zio/zio-logging/blob/master/docs/slf4j2-bridge.md, i recommend to rollback this change to not make it complicated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have completed rollback

@@ -84,6 +87,7 @@ object FileApp extends ZIOAppDefault {
|logger {
| format = "%timestamp{yyyy-MM-dd'T'HH:mm:ssZ} %fixed{7}{%level} [%fiberId] %name:%line %message %cause"
| path = "file:///tmp/file_app.log"
| rollingPolicy = TimeBasedRollingPolicy
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied

currentWriter
}
}
object TimeBasedRollingWriterProvider {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe you can add

val dateTimeFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")

here to companion object, so it will be crated just once, and you don't have to have it like argument in makePath

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I applied


private var timeInUse = makeNewTime
private var currentWriter: Writer = makeWriter(makePath(destination, timeInUse), charset, bufferedIOSize)
val formatter: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this used ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not use, I removed formatter in provider class

Comment on lines 41 to 57
override def writer: Writer = {
val newTime = makeNewTime
if (newTime != timeInUse) {
lock.lock()
try {
val newTimeInLock = makeNewTime
if (newTimeInLock != timeInUse) {
currentWriter.close()
currentWriter = makeWriter(makePath(destination, newTimeInLock), charset, bufferedIOSize)
timeInUse = newTimeInLock
}
} finally {
lock.unlock()
}
}
currentWriter
}
Copy link
Contributor Author

@SHSongs SHSongs Mar 24, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#691 (comment) continuously..

I applied a concurrent pattern and checked the time under the lock.
After acquiring the lock, I created a new timestamp value using makeNewTime(), because I think that if I were to use the time that was determined before acquiring the lock to create a new file, repeatedly create and close new writers.

A: 
set(newTime) -> lock -> if(newTime..) -> create 
B: 
set(newTime) -> lock -> wait                 -> if(newTime..) -> create

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

   override def writer: Writer = {
      val newTime = makeNewTime
      if (newTime != timeInUse) { // if they are same it will not go inside if, so it will not be locked
        lock.lock()
        try {

// considering that time is date, basically it should not happen that newTimeInLock != newTime
// otherwise it will have to wait for lock more almost one day
          val newTimeInLock = makeNewTime 
        
         // considering proposed    if (newTime != timeInUse) this additional check here in lock,
         // this is for situation, when 2 threads at same time try to obtain lock, (after first if condition)
        // one will get lock first, and this will change writer and timeInUse
        // then after lock will be released, second one will get lock, and enter here
        // but timeInUse and writer are already changed, 
        // so it should not be needed to recreate basically same thing
          if (newTimeInLock != timeInUse) {
            currentWriter.close()
            currentWriter = makeWriter(makePath(destination, newTimeInLock), charset, bufferedIOSize)
            timeInUse = newTimeInLock
          }
        } finally {
          lock.unlock()
        }
      }
      currentWriter
    }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand the comments (timeInUse and writer are already changed), and I rewrote code.

final case class TimeBasedRollingWriterProvider(
destination: Path,
charset: Charset,
bufferedIOSize: Option[Int]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it will be good if we can test it

if we add here another attribute

makeNewTime: () => LocalDateTime = TimeBasedRollingWriterProvider.makeNewTime

then in WriterProviderSpec

it should be possible to do something like:

  val timeRef = new AtomicReference[LocalDateTime](LocalDateTime.now())
  
   timeRef.set(LocalDateTime.now().plusDays(1))
  
   val testMakeNewTime: () => LocalDateTime = () => timeRef.get()

and then test it if in case of parallel execution, when WriterProvider.writer is called multiple times with same time, if it return same writer

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wrote test code. Could you check this commit? 4fe78ca

sameWriter1 <- parallelExecution.map(_.toSet)
_ = timeRef.set(LocalDateTime.now().plusDays(1))
sameWriter2 <- parallelExecution.map(_.toSet)
} yield assertTrue(sameWriter1.size == 1 && sameWriter2.size == 1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good to add check sameWriter1 != sameWriter2

(makePath(destination4, localDateTime).toString == "/tmp/file.app.out-2023-03-21.log")
)
},
test("aa") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add better description of test case

val parallelExecution = ZIO
.foreachPar(1 to 5) { _ =>
ZIO.succeed(writerProvider.writer)
} @@ ZIOAspect.parallel(5)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZIOAspect.parallel(5) should not be needed

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 28, 2023

I rewrote code. Could you check this commit? 0aa8499

@justcoon
Copy link
Contributor

@SHSongs please run fmt and fix, CI is failing

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 28, 2023

@justcoon I ran fmt and fix, CI success

Copy link
Contributor

@justcoon justcoon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you very much for contribution

@justcoon
Copy link
Contributor

justcoon commented Mar 28, 2023

@SHSongs did you accepted license/cla ?

@SHSongs
Copy link
Contributor Author

SHSongs commented Mar 28, 2023

Screen Shot
@justcoon
Yes, I accepted license/cla. this is my Screen

@justcoon justcoon merged commit 071a1bf into zio:master Mar 28, 2023
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

Successfully merging this pull request may close these issues.

2 participants