Osy#betaRun it on Osyrin: osyrin.com

Reference / Config

exporting your app's logs (app.LogExport)

app.LogExport = LogBucket;

`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.

stable1 example compiled by CIconfigloggingstorageproviders

Summary#

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

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#

app.LogExport = <a class implementing IBlobStore>;

It is the same seam as your own file store (app.BlobStore), 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#

What is written#

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:

keycontent typecontent
logs/2026-10-21.jsonlapplication/x-ndjsonone 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#

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#

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#

Exactly as it calls a your own file store (app.BlobStore): 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#

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#

you writewhy
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 membersthe message names what is missing
an interface, an abstract or a generic class, or one with no parameterless constructorthe same rules as app.BlobStore

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).

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#

Related

your own file store (app.BlobStore)

`app.BlobStore` names a class of yours that keeps the app's file content — an S3 or R2 bucket, a table, anything you…

App configuration

The cross-cutting settings an app declares in one place — the app config object. Two kinds live here: the external…

declaring secrets (app.Secrets)

`app.Secrets` declares the named secrets your app uses — API keys, tokens, client secrets. Each is `new…