# exporting your app's logs (app.LogExport)

> `app.LogExport` names a class of yours implementing `IBlobStore` — an S3 or R2 bucket, a table, anything you can reach — and every finished UTC day of your app's own log lines is written there once, as `logs/{yyyy-MM-dd}.jsonl`. Use it to keep your logs longer than your plan keeps them, in storage you pay for.

<!-- id: config-log-export · area: config · stability: stable · html: https://osysharp.com/reference/config/log-export/ -->

## Summary        {#summary}

Name a class that implements `IBlobStore`, and each day of your app's `Log.*` lines is copied into it after the day ends.

```osy syntax
using Osysharp.Storage;

class LogBucket : IBlobStore {
  public void Put(string key, byte[] content, string contentType) { … }
  public byte[]? Get(string key) { … }
  public bool Exists(string key) { … }
  public void Delete(string key) { … }
}

app.LogExport = LogBucket;      // the CLASS — the platform constructs it
```

The platform keeps your app's log lines for a limited time — your plan's log retention, 7 days on the free plan and 30
on a paid one — and promises nothing past it. An export is how you keep them longer: the copy in your storage is yours,
and nothing trims it.

## Signature      {#signature}

```osy syntax
app.LogExport = <a class implementing IBlobStore>;
```

It is the same seam as [your own file store (app.BlobStore)](https://osysharp.com/reference/config/blob-store/), with the same rules for what a registrable class is. It can be the very
class your app keeps its files in, or a different one pointed at a different bucket.

## Description    {#description}

### What is written   {#what}

Once a day, shortly after midnight UTC, your app's runtime writes every finished UTC day of the last week that your
store does not have yet, each under its own key:

| key | content type | content |
|---|---|---|
| `logs/2026-10-21.jsonl` | `application/x-ndjson` | one JSON object per line, oldest first |

Each line carries the fields the Logs tab shows: `at` (UTC), `level`, `message`, `correlationId`, `functionName`,
`workflowId`, `agentName`, `userId`, `source` (`Osy.App` for a server line, `Osy.Client` for one from the browser) and
`exception`.

Only lines that belong to your app are written — what your server and client code logged with `Log.*`, and the lines
the platform wrote about your app's own requests and faults. Another application's line is never in your file.

### Once per day   {#once}

Before writing a day, the platform asks your store's `Exists` for its key, and a day already there is skipped. So a
retry, a restart or two workers running at once never write a day twice, and your store never has to handle a
duplicate. A day on which nothing was logged is written as an empty file, so "nothing happened" and "not exported"
are different answers in your bucket.

Today is still being written, so it is exported tomorrow. A run looks back seven days, which is how an export catches
up after your store or the platform was unavailable — the lines are still within your plan's retention then.

### Very busy days   {#limits}

A day is written as one file of at most 100,000 lines. A day with more ends with a line
`{"truncated":true,"lines":100000,…}`, so you are told rather than handed a file that silently stops short.

### How the platform calls your class   {#calls}

Exactly as it calls a [your own file store (app.BlobStore)](https://osysharp.com/reference/config/blob-store/): it constructs the class for each call (field initializers, then the
parameterless constructor), so read a bucket name or a credential from `Config.*` and `Secret.*` there. Each member
runs to completion, and what it writes is saved when it returns.

### When it fails   {#failures}

If your store refuses a write, the days not yet written are tried again six hours later; a day already written is
never written again. Each failed attempt is logged once, as a warning naming your app.

### What is refused   {#refused}

| you write | why |
|---|---|
| `app.LogExport = new LogBucket();` | name the class; the platform has to be able to construct it again whenever it needs one |
| a class that does not implement `IBlobStore`, or leaves out one of its members | the message names what is missing |
| an interface, an `abstract` or a generic class, or one with no parameterless constructor | the same rules as `app.BlobStore` |

## Examples       {#examples}

An export into one of the app's own tables — the shape the platform's own tests use. In production you would point a
store at a bucket instead, for example the `Osysharp.S3` store shown on [your own file store (app.BlobStore)](https://osysharp.com/reference/config/blob-store/).

```osy test app=log-export-table
using Osysharp.Storage;

entity ExportedDay {
  [Unique, MaxLength(400)] string Key;
  byte[] Content;
  [MaxLength(200)] string ContentType;
}

class LogBucket : IBlobStore {
  public void Put(string key, byte[] content, string contentType) {
    new ExportedDay { Key = key, Content = content, ContentType = contentType };
  }

  public byte[]? Get(string key) {
    var row = ExportedDay.FirstOrDefault(b => b.Key == key);
    return row == null ? null : row.Content;
  }

  public bool Exists(string key) => ExportedDay.Any(b => b.Key == key);

  public void Delete(string key) {
    var row = ExportedDay.FirstOrDefault(b => b.Key == key);
    if (row != null) row.Delete();
  }
}

app.LogExport = LogBucket;
```

## See also       {#see-also}
- [your own file store (app.BlobStore)](https://osysharp.com/reference/config/blob-store/) — the same seam, for where your app's file content lives
- [declaring secrets (app.Secrets)](https://osysharp.com/reference/config/secrets/) — where a store's bucket credentials come from
- [App configuration](https://osysharp.com/reference/config/index/) — the other things an app declares once
