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 itThe 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:
| 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#
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 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#
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#
- your own file store (app.BlobStore) — the same seam, for where your app's file content lives
- declaring secrets (app.Secrets) — where a store's bucket credentials come from
- App configuration — the other things an app declares once