' . parent::printLog() . '
';
+ }
+
+ /**
+ * @param EntryInterface|null $entry
+ * @return string
+ * @throws \Exception
+ */
+ protected function printEntry(?EntryInterface $entry = null): string
+ {
+ $entry = $entry ?? $this->entry;
+ /** @var Entry $entry */
+ $return = '';
+ $first = true;
+ foreach ($entry as $line) {
+ $entryClass = "entry-no-error";
+ if ($entry->getLevel()->asInt() <= Level::ERROR->asInt()) {
+ $entryClass = "entry-error";
+ }
+ $return .= '';
+ $return .= '
';
+ $return .= '
';
+ $lineString = $this->printLine($line);
+ if ($entry->getPrefix() !== null) {
+ $prefix = htmlentities($entry->getPrefix());
+ $lineString = str_replace($prefix, '' . $prefix . '', $lineString);
+ }
+ $return .= $lineString;
+ $return .= '
';
+ $return .= '
';
+ $first = false;
+ }
+
+ return $return;
+ }
+
+ /**
+ * @param LineInterface $line
+ * @return string
+ */
+ protected function printLine(LineInterface $line): string
+ {
+ return $this->runModifications(htmlentities($line->getText())) . PHP_EOL;
+ }
+}
diff --git a/src/Router/Action.php b/src/Router/Action.php
new file mode 100644
index 0000000..0accfc3
--- /dev/null
+++ b/src/Router/Action.php
@@ -0,0 +1,8 @@
+getMethod() !== $method) {
+ return false;
+ }
+ return preg_match($this->getPattern(), $path) === 1;
+ }
+
+ /**
+ * @return Method
+ */
+ public function getMethod(): Method
+ {
+ return $this->method;
+ }
+
+ /**
+ * @return string
+ */
+ public function getPattern(): string
+ {
+ return $this->pattern;
+ }
+
+ /**
+ * @return Action
+ */
+ public function getAction(): Action
+ {
+ return $this->action;
+ }
+}
\ No newline at end of file
diff --git a/src/Router/Router.php b/src/Router/Router.php
new file mode 100644
index 0000000..2126f68
--- /dev/null
+++ b/src/Router/Router.php
@@ -0,0 +1,73 @@
+routes[] = new Route($method, $pattern, $action);
+ return $this;
+ }
+
+ /**
+ * @param Action $defaultAction
+ * @return $this
+ */
+ public function setDefaultAction(Action $defaultAction): static
+ {
+ $this->defaultAction = $defaultAction;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function run(): static
+ {
+ $route = $this->findRoute();
+ if (!$route) {
+ $this->defaultAction?->run();
+ return $this;
+ }
+ $result = $route->getAction()->run();
+ if (!$result) {
+ $this->defaultAction?->run();
+ }
+ return $this;
+ }
+
+ /**
+ * @return Route|null
+ */
+ protected function findRoute(): ?Route
+ {
+ $path = URL::getCurrent()->getPath();
+ $method = Method::getCurrent();
+
+ foreach ($this->routes as $route) {
+ if ($route->matches($method, $path)) {
+ return $route;
+ }
+ }
+ return null;
+ }
+}
diff --git a/src/Storage/MongoDBClient.php b/src/Storage/MongoDBClient.php
new file mode 100644
index 0000000..43e3079
--- /dev/null
+++ b/src/Storage/MongoDBClient.php
@@ -0,0 +1,234 @@
+get(ConfigKey::MONGODB_URL);
+ $url = new Uri($configUrl);
+ $query = $url->getQuery();
+ $queryParams = [];
+ if ($query !== null) {
+ parse_str($query, $queryParams);
+ }
+ if (!isset($queryParams['serverSelectionTimeoutMS'])) {
+ $queryParams['serverSelectionTimeoutMS'] = 5_000;
+ }
+ if (!isset($queryParams['socketTimeoutMS'])) {
+ $queryParams['socketTimeoutMS'] = 60_000;
+ }
+ $newQuery = http_build_query($queryParams);
+ $newUrl = $url->withQuery($newQuery);
+ return $newUrl->toString();
+ }
+
+ /**
+ * Connect to MongoDB
+ */
+ protected function connect(): void
+ {
+ if ($this->connection === null) {
+ $config = Config::getInstance();
+ $this->connection = new Client($this->getConnectionURL());
+ $this->database = $this->connection->getDatabase($config->get(ConfigKey::MONGODB_DATABASE));
+ }
+ }
+
+ /**
+ * Ensure indexes exist
+ *
+ * @return void
+ */
+ public function ensureIndexes(): void
+ {
+ $logs = $this->getLogsCollection();
+ $logs->createIndex(['expires' => 1], ['expireAfterSeconds' => 0]);
+
+ $cache = $this->getCacheCollection();
+ $cache->createIndex(['expires' => 1], ['expireAfterSeconds' => 0]);
+ }
+
+ /**
+ * @return void
+ */
+ public function reset(): void
+ {
+ $this->connection = null;
+ $this->logs = null;
+ $this->cache = null;
+ }
+
+ /**
+ * Get the collection for logs
+ *
+ * @return Collection
+ */
+ public function getLogsCollection(): Collection
+ {
+ if ($this->logs === null) {
+ $this->connect();
+ $this->logs = $this->database->getCollection('logs');
+ }
+ return $this->logs;
+ }
+
+ /**
+ * @param string $id
+ * @param bool $includeContent
+ * @return object|null
+ */
+ public function findLog(string $id, bool $includeContent = true): ?object
+ {
+ $options = [];
+ if (!$includeContent) {
+ $options['projection'] = ['data' => 0];
+ }
+
+ $collection = $this->getLogsCollection();
+ $result = $collection->findOne(['_id' => $id], $options);
+ if ($result === null) {
+ // Check for legacy ID without the first character
+ return $collection->findOne(['_id' => substr($id, 1)], $options);
+ }
+ return $result;
+ }
+
+ /**
+ * @param string[] $ids
+ * @param bool $includeContent
+ * @return object[]
+ */
+ public function findLogs(array $ids, bool $includeContent = true): array
+ {
+ $options = [];
+ if (!$includeContent) {
+ $options['projection'] = ['data' => 0];
+ }
+
+ $collection = $this->getLogsCollection();
+ $results = $collection->find(['_id' => ['$in' => $ids]], $options)->toArray();
+ $foundIds = [];
+ foreach ($results as $result) {
+ $foundIds[] = (string)$result->_id;
+ }
+
+ $missingIds = array_diff($ids, $foundIds);
+ if (!empty($missingIds)) {
+ $legacyIds = [];
+ foreach ($missingIds as $id) {
+ $legacyIds[substr($id, 1)] = $id;
+ }
+
+ // Check for legacy IDs without the first character
+ $legacyResults = $collection->find(['_id' => ['$in' => array_keys($legacyIds)]], $options)->toArray();
+ foreach ($legacyResults as $result) {
+ // Map the legacy ID back to the original ID
+ $originalId = $legacyIds[(string)$result->_id];
+ $result->_id = $originalId;
+
+ // Add the found legacy results to the main results array
+ $results[] = $result;
+ }
+ }
+ return $results;
+ }
+
+ /**
+ * @param string $id
+ * @return bool
+ */
+ public function deleteLog(string $id): bool
+ {
+ $collection = $this->getLogsCollection();
+ $result = $collection->deleteOne(['_id' => $id]);
+ if ($result->getDeletedCount() === 0) {
+ // Check for legacy ID without the first character
+ $result = $collection->deleteOne(['_id' => substr($id, 1)]);
+ return $result->getDeletedCount() === 1;
+ }
+ return true;
+ }
+
+ /**
+ * @param array $ids
+ * @return int Number of logs deleted
+ */
+ public function deleteLogs(array $ids): int
+ {
+ $collection = $this->getLogsCollection();
+ $result = $collection->deleteMany(['_id' => ['$in' => $ids]]);
+ $deletedCount = $result->getDeletedCount();
+
+ if ($deletedCount === count($ids)) {
+ return $deletedCount;
+ }
+
+ // Check for legacy IDs without the first character
+ $legacyIds = [];
+ foreach ($ids as $id) {
+ $legacyIds[] = substr($id, 1);
+ }
+ $legacyResult = $collection->deleteMany(['_id' => ['$in' => $legacyIds]]);
+ return $deletedCount + $legacyResult->getDeletedCount();
+ }
+
+ /**
+ * @param string $id
+ * @return bool
+ */
+ public function hasLog(string $id): bool
+ {
+ return $this->findLog($id) !== null;
+ }
+
+ /**
+ * @param string $id
+ * @param UTCDateTime $expires
+ * @return bool
+ */
+ public function setLogExpires(string $id, UTCDateTime $expires): bool
+ {
+ $collection = $this->getLogsCollection();
+ $result = $collection->updateOne(
+ ['_id' => $id],
+ ['$set' => ['expires' => $expires]]
+ );
+ return $result->getModifiedCount() === 1;
+ }
+
+ /**
+ * Get the collection for caching
+ *
+ * @return Collection
+ */
+ public function getCacheCollection(): Collection
+ {
+ if ($this->cache === null) {
+ $this->connect();
+ $this->cache = $this->database->getCollection('cache');
+ }
+ return $this->cache;
+ }
+}
diff --git a/src/Util/Singleton.php b/src/Util/Singleton.php
new file mode 100644
index 0000000..26d1bbd
--- /dev/null
+++ b/src/Util/Singleton.php
@@ -0,0 +1,37 @@
+ 365 * 24 * 60 * 60,
+ "month" => 30 * 24 * 60 * 60,
+ "week" => 7 * 24 * 60 * 60,
+ "day" => 24 * 60 * 60,
+ "hour" => 60 * 60,
+ "minute" => 60,
+ "second" => 1,
+ ];
+
+ /**
+ * @param int $value
+ * @param string $unit
+ * @return string
+ */
+ protected function formatUnit(int $value, string $unit): string
+ {
+ if ($value === 1) {
+ return $value . " " . $unit;
+ } else {
+ return $value . " " . $unit . "s";
+ }
+ }
+
+ /**
+ * @param int $duration
+ * @param string $separator
+ * @return string
+ */
+ public function format(int $duration, string $separator = ", "): string
+ {
+ $parts = [];
+ while ($duration > 0) {
+ foreach (self::UNITS as $unit => $seconds) {
+ if ($duration >= $seconds) {
+ $value = intdiv($duration, $seconds);
+ $duration -= $value * $seconds;
+ $parts[] = $this->formatUnit($value, $unit);
+ break;
+ }
+ }
+ }
+ return implode($separator, $parts);
+ }
+}
diff --git a/src/Util/URL.php b/src/Util/URL.php
new file mode 100644
index 0000000..d47ee72
--- /dev/null
+++ b/src/Util/URL.php
@@ -0,0 +1,128 @@
+withHost(static::API_SUBDOMAIN . $base->getHost());
+ }
+
+ /**
+ * @return Uri
+ */
+ public static function getCurrent(): Uri
+ {
+ if (static::$current) {
+ return static::$current;
+ }
+ $scheme = $_SERVER['REQUEST_SCHEME'];
+ $host = $_SERVER['HTTP_HOST'];
+ $requestUri = $_SERVER['REQUEST_URI'];
+ return static::$current = new Uri("$scheme://$host$requestUri");
+ }
+
+ /**
+ * @return bool
+ */
+ public static function isApi(): bool
+ {
+ $currentHost = static::getCurrent()->getHost();
+ $apiHost = static::getApi()->getHost();
+ return $currentHost === $apiHost;
+ }
+
+ /**
+ * @return string
+ */
+ public static function getLastPathPart(): string
+ {
+ $path = static::getCurrent()->getPath();
+ $parts = explode("/", $path);
+ do {
+ $part = trim(array_pop($parts));
+ } while ($part === "" && count($parts) > 0);
+ return $part;
+ }
+}
\ No newline at end of file
diff --git a/web/frontend/404.php b/web/frontend/404.php
new file mode 100644
index 0000000..e1b1d57
--- /dev/null
+++ b/web/frontend/404.php
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
Create a log
+
+
+ POST = htmlspecialchars(URL::getApi()->withPath("/1/log")->toString()); ?> application/json
+
+
+ Posting content with the content type application/x-www-form-urlencoded is still supported for backwards compatibility, but does not support setting metadata.
+
+
+
+ | Field |
+ Required |
+ Type |
+ Description |
+
+
+ | content |
+ |
+ string |
+
+ The raw log file content as string.
+ Limited to = number_format($config->get(ConfigKey::STORAGE_LIMIT_BYTES) / 1024 / 1024, 2); ?> MiB and = number_format($config->get(ConfigKey::STORAGE_LIMIT_LINES)); ?> lines.
+ Will be truncated if possible and necessary, but truncating on the client side is recommended.
+ |
+
+
+ | source |
+ |
+ string |
+ The name of the source, e.g. a domain or software name. |
+
+
+ | metadata |
+ |
+ array |
+ An array of metadata entries. |
+
+
+
+
Example body application/json
+
{
+ "content": "[log file content...]",
+ "source": "example.org"
+}
+
+
Metadata
+
+ You can send metadata alongside the log content to be displayed on the log page and/or be read by other applications through this API.
+ This is entirely optional, but can help to provide additional context, e.g. internal server IDs, software versions etc.
+
+
+ A metadata entry is an object with the following fields:
+
+
+
+ | Field |
+ Required |
+ Type |
+ Description |
+
+
+ | key |
+ |
+ string |
+ The metadata key. Can be used to identify the entry in your code later. |
+
+
+ | value |
+ |
+ string|int|float|bool|null |
+ The metadata value. |
+
+
+ | label |
+ |
+ string |
+ The display label. If not provided, the key will be used as label. |
+
+
+ | visible |
+ |
+ bool |
+ Whether this metadata should be visible on the log page or is only available through the API. Default is true. |
+
+
+
+
Example body with metadata application/json
+
{
+ "content": "[log file content...]",
+ "source": "example.org",
+ "metadata": [
+ {
+ "key": "server_id",
+ "value": 12345,
+ "visible": false
+ },
+ {
+ "key": "software_version",
+ "value": "1.2.3",
+ "label": "Software Version",
+ "visible": true
+ }
+ ]
+}
+
+
Responses
+
Success application/json
+
+ The token provided in this response can be used to delete this log later. Store or discard it securely, it will not be shown again.
+
+
{
+ "success":true,
+ "id":"WnMMikq",
+ "source":null,
+ "created":1769597979,
+ "expires":1777373979,
+ "size":157369,
+ "lines":1201,
+ "errors":8,
+ "url": "= htmlspecialchars(URL::getBase()->withPath("/WnMMikq")->toString()); ?>",
+ "raw": "= htmlspecialchars(URL::getApi()->withPath("/1/raw/WnMMikq")->toString()); ?>",
+ "token":"78351fafe495398163fff847f9a26dda440435dcf7b5f92e8e36308f3683d771",
+ "metadata": [
+ {
+ "key": "server_id",
+ "value": 12345,
+ "visible": false
+ },
+ {
+ "key": "software_version",
+ "value": "1.2.3",
+ "label": "Software Version",
+ "visible": true
+ }
+ ]
+}
+
Error application/json
+
+{
+ "success": false,
+ "error": "Required field 'content' not found."
+}
+
+
+
+
Get log info and content
+
+ GET = htmlspecialchars(URL::getApi()->toString()); ?>/1/log/[id]
+
+
+ This endpoint only returns the log info and metadata by default (same response as creating a log), you can also get the content in the same request by enabling it in different
+ formats using GET parameters. You can combine multiple parameters to get multiple content formats in one request, but keep in mind that this will
+ increase the response size.
+
+
+
+ | GET Parameter |
+ Response field |
+ Description |
+
+
+ | raw |
+ content.raw |
+ Includes the raw log content as string in the response. |
+
+
+ | parsed |
+ content.parsed |
+ Includes the parsed log content as array/objects in the response. |
+
+
+ | insights |
+ content.insights |
+ Includes the automatically detected insights in the response. |
+
+
+
Responses
+
Success application/json
+
+ All content fields are only included if the corresponding GET parameter is provided.
+ If no content parameter is provided, the entire content object is omitted from the response.
+
+
{
+ "success":true,
+ "id":"WnMMikq",
+ "source":null,
+ "created":1769597979,
+ "expires":1777373979,
+ "size":157369,
+ "lines":1201,
+ "errors":8,
+ "url": "= htmlspecialchars(URL::getBase()->withPath("/WnMMikq")->toString()); ?>",
+ "raw": "= htmlspecialchars(URL::getApi()->withPath("/1/raw/WnMMikq")->toString()); ?>",
+ "metadata": [
+ {
+ "key": "server_id",
+ "value": 12345,
+ "visible": false
+ },
+ {
+ "key": "software_version",
+ "value": "1.2.3",
+ "label": "Software Version",
+ "visible": true
+ }
+ ],
+ "content": {
+ "raw": "[log file content...]",
+ "parsed": [ /* parsed log entries */ ],
+ "insights": { "problems": [ /* detected problems */ ], "information": [ /* detected information */ ] }
+ }
+}
+
Error application/json
+
+{
+ "success": false,
+ "error": "Log not found."
+}
+
+
+
Delete a log
+
+ Deleting a log requires the token that was provided when creating the log.
+
+
+
+ DELETE = htmlspecialchars(URL::getApi()->toString()); ?>/1/log/[id]
+
+
+
Headers
+
+
+ | Header |
+ Example |
+ Description |
+
+
+ | Authorization |
+ Authorization: Bearer 78351fafe495398163f... |
+ The type (always "Bearer") and the log token received when creating the log. |
+
+
+
+
Responses
+
Success application/json
+
{
+ "success": true
+}
+
Error application/json
+
+{
+ "success": false,
+ "error": "Invalid token."
+}
+
+
+
Bulk delete multiple logs
+
+ This method allows deleting up to = BulkDeleteLogsAction::MAX_IDS; ?> at once.
+ Deleting logs requires the tokens that were provided when the logs were created.
+
+
+
+ POST = htmlspecialchars(URL::getApi()->toString()); ?>/1/bulk/log/delete
+
+
+
Example body application/json
+
= json_encode([
+ [
+ "id" => "6wexMDE",
+ "token" => "78351fafe495398163fff847f9a26dda440435dcf7b5f92e8e36308f3683d771"
+ ],
+ [
+ "id" => "OahzhMG",
+ "token" => "6520dd42ec3d5fd0e83f28220974fb83d3bdc0746853f5022373f8e5b062651b"
+ ],
+ ], JSON_PRETTY_PRINT); ?>
+
+
Responses
+
Success application/json
+
+ The bulk delete request will return a successful result and status code 207,
+ indicating that the request was processed.
+ Results for the individual operations are included in the response body.
+
+
=json_encode(new MultiResponse()
+ ->addResponse("6wexMDE", new ApiResponse())
+ ->addResponse("OahzhMG", new ApiResponse()), JSON_PRETTY_PRINT); ?>
+
Partial success application/json
+
+ If a bulk delete request is valid, but not all logs can be deleted (e.g. due to invalid tokens or non-existing logs),
+ it will still overall be considered successful, but the response body will include error results for the logs that could not be deleted.
+
+
=json_encode(new MultiResponse()
+ ->addResponse("6wexMDE", new ApiResponse())
+ ->addResponse("OahzhMG", new ApiError(404, "Log not found.")), JSON_PRETTY_PRINT); ?>
+
Error application/json
+
+ If a bulk delete request is malformed or invalid, the entire request will be
+ rejected with an error response and no logs will be deleted.
+
+
+{
+ "success": false,
+ "error": "No logs provided."
+}
+
+
+
Get the raw log file content
+
+ Only use this endpoint if you really only need the raw log content. For most use cases, getting the log info and content together from the log endpoint is recommended.
+
+
+ GET = htmlspecialchars(URL::getApi()->toString()); ?>/1/raw/[id]
+
+
+
+ | Field |
+ Type |
+ Description |
+
+
+ | [id] |
+ string |
+ The log file id, received from the paste endpoint or from a URL (= htmlspecialchars(URL::getBase()->toString()); ?>/[id]). |
+
+
+
+
Success text/plain
+
+[18:25:33] [Server thread/INFO]: Starting minecraft server version 1.16.2
+[18:25:33] [Server thread/INFO]: Loading properties
+[18:25:34] [Server thread/INFO]: Default game type: SURVIVAL
+...
+
+
Error application/json
+
+{
+ "success": false,
+ "error": "Log not found."
+}
+
+
+
Get insights
+
+ This endpoint is mainly kept for backwards compatibility. For new applications, getting the insights together with the log info from the log endpoint is recommended.
+
+
+ GET = htmlspecialchars(URL::getApi()->toString()); ?>/1/insights/[id]
+
+
+
+ | Field |
+ Type |
+ Description |
+
+
+ | [id] |
+ string |
+ The log file id, received from the paste endpoint or from a URL (= htmlspecialchars(URL::getBase()->toString()); ?>/[id]). |
+
+
+
+
Success application/json
+
+{
+ "id": "name/type",
+ "name": "Software name, e.g. Vanilla",
+ "type": "Type name, e.g. Server Log",
+ "version": "Version, e.g. 1.12.2",
+ "title": "Combined title, e.g. Vanilla 1.12.2 Server Log",
+ "analysis": {
+ "problems": [
+ {
+ "message": "A message explaining the problem.",
+ "counter": 1,
+ "entry": {
+ "level": 6,
+ "time": null,
+ "prefix": "The prefix of this entry, usually the part containing time and loglevel.",
+ "lines": [
+ {
+ "number": 1,
+ "content": "The full content of the line."
+ }
+ ]
+ },
+ "solutions": [
+ {
+ "message": "A message explaining a possible solution."
+ }
+ ]
+ }
+ ],
+ "information": [
+ {
+ "message": "Label: value",
+ "counter": 1,
+ "label": "The label of this information, e.g. Minecraft version",
+ "value": "The value of this information, e.g. 1.12.2",
+ "entry": {
+ "level": 6,
+ "time": null,
+ "prefix": "The prefix of this entry, usually the part containing time and loglevel.",
+ "lines": [
+ {
+ "number": 6,
+ "content": "The full content of the line."
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
+
Error application/json
+
+{
+ "success": false,
+ "error": "Log not found."
+}
+
+
+
Analyse a log without saving it
+
+ If you only want to use the analysis features of this service without saving the log, you can use this endpoint.
+ Please do not save logs that you only want to analyse, as this wastes storage space and resources.
+
+
+
+ POST = htmlspecialchars(URL::getApi()->withPath("/1/analyse")->toString()); ?> application/x-www-form-urlencoded application/json
+
+
+
+ | Field |
+ Type |
+ Description |
+
+
+ | content |
+ string |
+ The raw log file content as string. Maximum length is 10MiB and 25k lines, will be shortened if necessary. |
+
+
+
+
Success application/json
+
+{
+ "id": "name/type",
+ "name": "Software name, e.g. Vanilla",
+ "type": "Type name, e.g. Server Log",
+ "version": "Version, e.g. 1.12.2",
+ "title": "Combined title, e.g. Vanilla 1.12.2 Server Log",
+ "analysis": {
+ "problems": [
+ {
+ "message": "A message explaining the problem.",
+ "counter": 1,
+ "entry": {
+ "level": 6,
+ "time": null,
+ "prefix": "The prefix of this entry, usually the part containing time and loglevel.",
+ "lines": [
+ {
+ "number": 1,
+ "content": "The full content of the line."
+ }
+ ]
+ },
+ "solutions": [
+ {
+ "message": "A message explaining a possible solution."
+ }
+ ]
+ }
+ ],
+ "information": [
+ {
+ "message": "Label: value",
+ "counter": 1,
+ "label": "The label of this information, e.g. Minecraft version",
+ "value": "The value of this information, e.g. 1.12.2",
+ "entry": {
+ "level": 6,
+ "time": null,
+ "prefix": "The prefix of this entry, usually the part containing time and loglevel.",
+ "lines": [
+ {
+ "number": 6,
+ "content": "The full content of the line."
+ }
+ ]
+ }
+ }
+ ]
+ }
+}
+
Error application/json
+
+{
+ "success": false,
+ "error": "Required field 'content' is empty."
+}
+
+
+
Check storage limits
+
+
+ GET = htmlspecialchars(URL::getApi()->withPath("/1/limits")->toString()); ?>
+
+
Success application/json
+
+{
+ "storageTime": 7776000,
+ "maxLength": 10485760,
+ "maxLines": 25000
+}
+
+
+ | Field |
+ Type |
+ Description |
+
+
+ | storageTime |
+ integer |
+ The duration in seconds that a log is stored for after the last view. |
+
+
+ | maxLength |
+ integer |
+ Maximum file length in bytes. Logs over this limit will be truncated to this length. |
+
+
+ | maxLines |
+ integer |
+ Maximum number of lines. Additional lines will be removed. |
+
+
+
+
+
Get filters
+
+ Filters modify the log content before storing it. They are applied automatically when creating a new log on the server side.
+ You can get a list of active filters from this endpoint if you want to apply the same filters on the client side before uploading a log.
+
+
+ GET = htmlspecialchars(URL::getApi()->withPath("/1/filters")->toString()); ?>
+
+
Success application/json
+
+=htmlspecialchars(json_encode(\Aternos\Mclogs\Filter\Filter::getAll(), JSON_PRETTY_PRINT)); ?>
+
Filter types
+
+
+ | Type |
+ Description |
+
+
+ | trim |
+
+ Trim any whitespace characters from the beginning and end of the log content.
+ |
+
+
+ | limit-bytes |
+
+ Limit the log content to a maximum number of bytes (data.limit). Content exceeding this limit will be truncated.
+ |
+
+
+ | limit-lines |
+
+ Limit the log content to a maximum number of lines (data.limit). Additional lines will be removed.
+ |
+
+
+ | regex |
+
+ Apply regular expression replacements to the log content. Each pattern in data.patterns will be applied in order and replaced with the provided replacement, unless the matched string matches one of the exemption patterns in data.exemptions.
+ |
+
+
+
+ Make sure to handle any filter error, e.g. unknown filter types gracefully, as new filter types may be added in the future.
+
+
+
+
+
Notes
+
The API has currently a rate limit of 60 requests per minute per IP address. This is set to ensure the operability of this service. If you have any use case that requires a higher limit, feel free to contact us.
+
+
+
+
+
+
+
diff --git a/web/frontend/log.php b/web/frontend/log.php
new file mode 100644
index 0000000..97c5fb1
--- /dev/null
+++ b/web/frontend/log.php
@@ -0,0 +1,230 @@
+
+
+
+
+