• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / mender / 2413052326

27 Mar 2026 09:43AM UTC coverage: 81.686% (+0.3%) from 81.405%
2413052326

push

gitlab-ci

vpodzime
feat: Large deployment logs are now trimmed to be accepted by the server

Deployment logs larger than 1 MiB are rejected by the
server which leads to two issues:

- excessive bandwith consumption when uploading such large logs
  only to be thrown away, and

- no deployment logs for particular device and particular failed
  deployment available at the server at all.

To prevent this, the client now trims large deployment logs and
only sends the biggest possible part of the logs from their end.

Ticket: MEN-9415
Changelog: title
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>

50 of 60 new or added lines in 2 files covered. (83.33%)

54 existing lines in 1 file now uncovered.

9059 of 11090 relevant lines covered (81.69%)

20531.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.71
/src/mender-update/deployments/deployments.cpp
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
#include <mender-update/deployments.hpp>
16

17
#include <algorithm>
18
#include <sstream>
19
#include <string>
20

21
#include <api/api.hpp>
22
#include <api/client.hpp>
23
#include <common/common.hpp>
24
#include <common/error.hpp>
25
#include <common/events.hpp>
26
#include <common/expected.hpp>
27
#include <common/http.hpp>
28
#include <common/io.hpp>
29
#include <common/json.hpp>
30
#include <common/log.hpp>
31
#include <common/optional.hpp>
32
#include <common/path.hpp>
33
#include <mender-update/context.hpp>
34

35
namespace mender {
36
namespace update {
37
namespace deployments {
38

39
using namespace std;
40

41
namespace api = mender::api;
42
namespace common = mender::common;
43
namespace context = mender::update::context;
44
namespace error = mender::common::error;
45
namespace events = mender::common::events;
46
namespace expected = mender::common::expected;
47
namespace http = mender::common::http;
48
namespace io = mender::common::io;
49
namespace json = mender::common::json;
50
namespace log = mender::common::log;
51
namespace path = mender::common::path;
52

53
using ExpectedOffset = expected::expected<ifstream::off_type, error::Error>;
54

55
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
56

UNCOV
57
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
UNCOV
58
        return "DeploymentsErrorCategory";
×
59
}
60

61
string DeploymentsErrorCategoryClass::message(int code) const {
33✔
62
        switch (code) {
33✔
63
        case NoError:
UNCOV
64
                return "Success";
×
65
        case InvalidDataError:
66
                return "Invalid data error";
×
67
        case BadResponseError:
68
                return "Bad response error";
4✔
69
        case DeploymentAbortedError:
70
                return "Deployment was aborted on the server";
3✔
71
        case TooManyRequestsError:
72
                return "Too many requests";
26✔
73
        case RequestBodyTooLargeError:
UNCOV
74
                return "Request body too large";
×
75
        }
76
        assert(false);
UNCOV
77
        return "Unknown";
×
78
}
79

80
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
89✔
81
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
104✔
82
}
83

84
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
85
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
86

87
error::Error DeploymentClient::CheckNewDeployments(
10✔
88
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
89
        auto ex_compatible_type = ctx.GetCompatibleType();
20✔
90
        if (!ex_compatible_type) {
10✔
91
                return ex_compatible_type.error();
4✔
92
        }
93
        string compatible_type = ex_compatible_type.value();
6✔
94

95
        auto ex_provides = ctx.LoadProvides();
6✔
96
        if (!ex_provides) {
6✔
UNCOV
97
                return ex_provides.error();
×
98
        }
99
        auto provides = ex_provides.value();
6✔
100
        if (provides.find("artifact_name") == provides.end()) {
12✔
UNCOV
101
                return MakeError(InvalidDataError, "Missing artifact name data");
×
102
        }
103

104
        stringstream ss;
6✔
105
        ss << R"({"device_provides":{)";
6✔
106
        ss << R"("device_type":")";
6✔
107
        ss << json::EscapeString(compatible_type);
12✔
108

109
        for (const auto &kv : provides) {
14✔
110
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
8✔
111
                ss << json::EscapeString(kv.second);
16✔
112
        }
113

114
        ss << R"("}})";
6✔
115

116
        string v2_payload = ss.str();
117
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
118
        http::BodyGenerator payload_gen = [v2_payload]() {
54✔
119
                return make_shared<io::StringReader>(v2_payload);
6✔
120
        };
6✔
121

122
        auto v2_req = make_shared<api::APIRequest>();
6✔
123
        v2_req->SetPath(check_updates_v2_uri);
124
        v2_req->SetMethod(http::Method::POST);
6✔
125
        v2_req->SetHeader("Content-Type", "application/json");
12✔
126
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
127
        v2_req->SetHeader("Accept", "application/json");
12✔
128
        v2_req->SetBodyGenerator(payload_gen);
6✔
129

130
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
12✔
131
                                         + "&device_type=" + http::URLEncode(compatible_type);
18✔
132
        auto v1_req = make_shared<api::APIRequest>();
6✔
133
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
6✔
134
        v1_req->SetMethod(http::Method::GET);
6✔
135
        v1_req->SetHeader("Accept", "application/json");
12✔
136

137
        auto received_body = make_shared<vector<uint8_t>>();
6✔
138
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
139
                if (status == http::StatusOK) {
4✔
140
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
141
                        if (ex_j) {
2✔
142
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
143
                                api_handler(response);
4✔
144
                        } else {
UNCOV
145
                                api_handler(expected::unexpected(
×
UNCOV
146
                                        CheckUpdatesAPIResponseError {status, nullopt, ex_j.error()}));
×
147
                        }
148
                } else if (status == http::StatusNoContent) {
2✔
149
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
150
                } else {
UNCOV
151
                        log::Warning(
×
152
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
153
                                + to_string(status));
×
UNCOV
154
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
155
                                status,
156
                                nullopt,
157
                                MakeError(
158
                                        DeploymentAbortedError,
UNCOV
159
                                        "received unhandled HTTP response: " + to_string(status))}));
×
160
                }
161
        };
10✔
162

163
        http::ResponseHandler header_handler =
164
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
165
                        this->HeaderHandler(received_body, api_handler, exp_resp);
27✔
166
                };
15✔
167

168
        http::ResponseHandler v1_body_handler =
169
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
15✔
170
                        if (!exp_resp) {
3✔
UNCOV
171
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
UNCOV
172
                                CheckUpdatesAPIResponse response = expected::unexpected(
×
173
                                        CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
174
                                api_handler(response);
×
175
                                return;
176
                        }
177
                        auto resp = exp_resp.value();
3✔
178
                        auto status = resp->GetStatusCode();
3✔
179

180
                        // StatusTooManyRequests must have been handled in HeaderHandler already
181
                        assert(status != http::StatusTooManyRequests);
182

183
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
184
                                handle_data(status);
2✔
185
                        } else {
186
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
187
                                string err_str;
188
                                if (ex_err_msg) {
1✔
UNCOV
189
                                        err_str = ex_err_msg.value();
×
190
                                } else {
191
                                        err_str = resp->GetStatusMessage();
2✔
192
                                }
193
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
194
                                        status,
195
                                        nullopt,
196
                                        MakeError(
197
                                                BadResponseError,
198
                                                "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
199
                        }
200
                };
6✔
201

202
        http::ResponseHandler v2_body_handler = [received_body,
18✔
203
                                                                                         v1_req,
204
                                                                                         header_handler,
205
                                                                                         v1_body_handler,
206
                                                                                         api_handler,
207
                                                                                         handle_data,
208
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
209
                if (!exp_resp) {
6✔
UNCOV
210
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
UNCOV
211
                        CheckUpdatesAPIResponse response = expected::unexpected(
×
212
                                CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
213
                        api_handler(response);
×
214
                        return;
215
                }
216
                auto resp = exp_resp.value();
6✔
217
                auto status = resp->GetStatusCode();
6✔
218

219
                // StatusTooManyRequests must have been handled in HeaderHandler already
220
                assert(status != http::StatusTooManyRequests);
221

222
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
223
                        handle_data(status);
2✔
224
                } else if (status == http::StatusNotFound) {
4✔
225
                        log::Debug(
3✔
226
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
227
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
228
                        if (err != error::NoError) {
3✔
UNCOV
229
                                api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
×
UNCOV
230
                                        status, nullopt, err.WithContext("While calling v1 endpoint")}));
×
231
                        }
232
                } else {
233
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
234
                        string err_str;
235
                        if (ex_err_msg) {
1✔
236
                                err_str = ex_err_msg.value();
1✔
237
                        } else {
UNCOV
238
                                err_str = resp->GetStatusMessage();
×
239
                        }
240
                        api_handler(expected::unexpected(CheckUpdatesAPIResponseError {
3✔
241
                                status,
242
                                nullopt,
243
                                MakeError(
244
                                        BadResponseError,
245
                                        "Got unexpected response " + to_string(status) + ": " + err_str)}));
2✔
246
                }
247
        };
6✔
248

249
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
250
}
12✔
251

252
void DeploymentClient::HeaderHandler(
12✔
253
        shared_ptr<vector<uint8_t>> received_body,
254
        CheckUpdatesAPIResponseHandler api_handler,
255
        http::ExpectedIncomingResponsePtr exp_resp) {
256
        if (!exp_resp) {
12✔
UNCOV
257
                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
258
                CheckUpdatesAPIResponse response =
259
                        expected::unexpected(CheckUpdatesAPIResponseError {nullopt, nullopt, exp_resp.error()});
×
UNCOV
260
                api_handler(response);
×
261
                return;
262
        }
263

264
        auto resp = exp_resp.value();
12✔
265
        auto status = resp->GetStatusCode();
12✔
266
        if (status == http::StatusTooManyRequests) {
12✔
267
                CheckUpdatesAPIResponse response = expected::unexpected(CheckUpdatesAPIResponseError {
6✔
268
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")});
9✔
269
                api_handler(response);
6✔
270
        }
271
        received_body->clear();
12✔
272
        auto body_writer = make_shared<io::ByteWriter>(received_body);
12✔
273
        body_writer->SetUnlimited(true);
12✔
274
        resp->SetBodyWriter(body_writer);
24✔
275
}
276

277
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
278
        "installing",
279
        "pause_before_installing",
280
        "downloading",
281
        "pause_before_rebooting",
282
        "rebooting",
283
        "pause_before_committing",
284
        "success",
285
        "failure",
286
        "already-installed"};
287

288
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
289
static const string status_uri_suffix = "/status";
290

291
string DeploymentStatusString(DeploymentStatus status) {
501✔
292
        return deployment_status_strings[static_cast<int>(status)];
505✔
293
}
294

295
error::Error DeploymentClient::PushStatus(
4✔
296
        const string &deployment_id,
297
        DeploymentStatus status,
298
        const string &substate,
299
        api::Client &client,
300
        StatusAPIResponseHandler api_handler) {
301
        // Cannot push a status update without a deployment ID
302
        AssertOrReturnError(deployment_id != "");
4✔
303
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
4✔
304
        if (substate != "") {
4✔
305
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
306
        } else {
307
                payload += "}";
1✔
308
        }
309
        http::BodyGenerator payload_gen = [payload]() {
36✔
310
                return make_shared<io::StringReader>(payload);
4✔
311
        };
4✔
312

313
        auto req = make_shared<api::APIRequest>();
4✔
314
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
4✔
315
        req->SetMethod(http::Method::PUT);
4✔
316
        req->SetHeader("Content-Type", "application/json");
8✔
317
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
318
        req->SetHeader("Accept", "application/json");
8✔
319
        req->SetBodyGenerator(payload_gen);
4✔
320

321
        auto received_body = make_shared<vector<uint8_t>>();
4✔
322
        return client.AsyncCall(
16✔
323
                req,
324
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
325
                        this->PushStatusHeaderHandler(received_body, api_handler, exp_resp);
12✔
326
                },
4✔
327
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
12✔
328
                        if (!exp_resp) {
4✔
UNCOV
329
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
UNCOV
330
                                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
331
                                return;
×
332
                        }
333

334
                        auto resp = exp_resp.value();
4✔
335
                        auto status = resp->GetStatusCode();
4✔
336

337
                        // StatusTooManyRequests must have been handled in PushStatusHeaderHandler already
338
                        assert(status != http::StatusTooManyRequests);
339

340
                        if (status == http::StatusNoContent) {
4✔
341
                                api_handler(StatusAPIResponse {status, nullopt, error::NoError});
2✔
342
                        } else if (status == http::StatusConflict) {
2✔
343
                                api_handler(StatusAPIResponse {
2✔
344
                                        status,
345
                                        nullopt,
346
                                        MakeError(DeploymentAbortedError, "Could not send status update to server")});
2✔
347
                        } else {
348
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
349
                                string err_str;
350
                                if (ex_err_msg) {
1✔
351
                                        err_str = ex_err_msg.value();
1✔
352
                                } else {
UNCOV
353
                                        err_str = resp->GetStatusMessage();
×
354
                                }
355
                                api_handler(StatusAPIResponse {
2✔
356
                                        status,
357
                                        nullopt,
358
                                        MakeError(
359
                                                BadResponseError,
360
                                                "Got unexpected response " + to_string(status)
1✔
361
                                                        + " from status API: " + err_str)});
2✔
362
                        }
363
                });
4✔
364
}
365

366
void DeploymentClient::PushStatusHeaderHandler(
7✔
367
        shared_ptr<vector<uint8_t>> received_body,
368
        StatusAPIResponseHandler api_handler,
369
        http::ExpectedIncomingResponsePtr exp_resp) {
370
        if (!exp_resp) {
7✔
UNCOV
371
                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
UNCOV
372
                api_handler(StatusAPIResponse {nullopt, nullopt, exp_resp.error()});
×
373
                return;
×
374
        }
375

376
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
377
        auto resp = exp_resp.value();
7✔
378
        auto status = resp->GetStatusCode();
7✔
379
        if (status == http::StatusTooManyRequests) {
7✔
380
                StatusAPIResponse response = {
381
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
382
                api_handler(response);
3✔
383
        }
3✔
384
        auto content_length = resp->GetHeader("Content-Length");
14✔
385
        if (!content_length) {
7✔
386
                log::Debug(
3✔
387
                        "Failed to get content length from the deployment status API response headers: "
388
                        + content_length.error().String());
6✔
389
                body_writer->SetUnlimited(true);
3✔
390
        } else {
391
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
392
                if (!ex_len) {
4✔
UNCOV
393
                        log::Error(
×
394
                                "Failed to convert the content length from the deployment status API response headers to an integer: "
395
                                + ex_len.error().String());
×
UNCOV
396
                        body_writer->SetUnlimited(true);
×
397
                } else {
398
                        received_body->resize(ex_len.value());
4✔
399
                }
400
        }
401
        resp->SetBodyWriter(body_writer);
14✔
402
}
403

404
using mender::common::expected::ExpectedSize;
405

406
static ExpectedSize GetLogFileDataSize(const string &path) {
23✔
407
        auto ex_istr = io::OpenIfstream(path);
23✔
408
        if (!ex_istr) {
23✔
UNCOV
409
                return expected::unexpected(ex_istr.error());
×
410
        }
411
        auto istr = std::move(ex_istr.value());
23✔
412

413
        // We want the size of the actual data without a potential trailing
414
        // comma. So let's seek one byte before the end of file, check if the last
415
        // byte is a comma and return the appropriate number.
416
        istr.seekg(-1, ios_base::end);
23✔
417
        int c = istr.get();
23✔
418
        if (c == ',') {
23✔
419
                return istr.tellg() - static_cast<ifstream::off_type>(1);
23✔
420
        } else {
UNCOV
421
                return istr.tellg();
×
422
        }
423
}
23✔
424

425
// Find the beginning of the next log message JSON after offset
426
static ExpectedOffset FindNextMsgAfter(const string &path, ifstream::off_type offset) {
1✔
427
        auto ex_is = io::OpenIfstream(path);
1✔
428
        if (!ex_is) {
1✔
NEW
429
                return expected::unexpected(ex_is.error());
×
430
        }
431
        auto is = std::move(ex_is.value());
1✔
432
        is.seekg(offset);
1✔
433
        int io_errno = errno;
1✔
434
        if (!is) {
1✔
NEW
435
                return expected::unexpected(error::Error(
×
NEW
436
                        generic_category().default_error_condition(io_errno),
×
NEW
437
                        "Failed to seek to truncated logs offset in '" + path + "'"));
×
438
        }
439

440
        // Now that we have seeked to the starting offset, we need to find the next
441
        // boundary between JSON log entries.
442
        const string pattern = R"(},{"timestamp")";
1✔
443
        string buf(1024, '\0');
444
        while (is.read(buf.data(), 1024)) {
1✔
445
                // make sure we don't work with some stale data from the previous read()
446
                buf.resize(is.gcount());
1✔
447

448
                // XXX: This is not perfect as it can fail to find the next boundary if
449
                //      it's split between the end of the current buffer contents and
450
                //      the start of the next chunk read from the file. However, with a
451
                //      1K buffer, this is very unlikely to happen and even if it does
452
                //      happen, the logs will just be trimmed a bit more than
453
                //      necessary. For the sake of incomparably simpler code that is
454
                //      easy to test.
455
                auto pos = buf.find(pattern);
1✔
456
                if (pos != string::npos) {
1✔
457
                        offset += pos + 2; // skip "},"
1✔
458
                        break;
1✔
459
                }
NEW
460
                offset += buf.size();
×
461
        }
462
        io_errno = errno;
1✔
463
        if (!is && !is.eof()) {
1✔
NEW
464
                return expected::unexpected(error::Error(
×
NEW
465
                        generic_category().default_error_condition(io_errno),
×
NEW
466
                        "Failed to read logs from '" + path + "'"));
×
467
        }
468

469
        // In case we read the whole rest of the file not finding the next JSON log
470
        // entry, there must be something really wrong with the log and it should be
471
        // fully skipped. The user will still get the information that the log was
472
        // truncated and they will still have the full log on the device.
473
        return offset;
474
}
1✔
475

476
const vector<uint8_t> JsonLogMessagesReader::header_ = {
477
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
478
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
479
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
480
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
481
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
482
const string JsonLogMessagesReader::too_much_data_msg_tmpl_ =
483
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "WARNING", "message": "(THE ORIGINAL LOGS WERE TOO BIG, THIS LOG IS TRUNCATED. The full log can be found on the device)"},)d";
484

485
JsonLogMessagesReader::~JsonLogMessagesReader() {
54✔
486
        reader_.reset();
487
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
18✔
488
                auto del_err = path::FileDelete(sanitized_fpath_);
18✔
489
                if (del_err != error::NoError) {
18✔
490
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
491
                }
492
        }
493
        sanitized_fpath_.erase();
18✔
494
}
36✔
495

496
static error::Error DoSanitizeLogs(
23✔
497
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
498
        auto ex_ifs = io::OpenIfstream(orig_path);
23✔
499
        if (!ex_ifs) {
23✔
UNCOV
500
                return ex_ifs.error();
×
501
        }
502
        auto ex_ofs = io::OpenOfstream(new_path);
23✔
503
        if (!ex_ofs) {
23✔
UNCOV
504
                return ex_ofs.error();
×
505
        }
506
        auto &ifs = ex_ifs.value();
23✔
507
        auto &ofs = ex_ofs.value();
23✔
508

509
        string last_known_tstamp = first_tstamp;
23✔
510
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
23✔
511
        const string corrupt_msg_suffix_data =
512
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
23✔
513

514
        string line;
515
        first_tstamp.erase();
23✔
516
        all_valid = true;
23✔
517
        error::Error err;
23✔
518
        while (!ifs.eof()) {
20,109✔
519
                getline(ifs, line);
20,086✔
520
                if (!ifs.eof() && !ifs) {
20,086✔
521
                        int io_errno = errno;
×
522
                        return error::Error(
UNCOV
523
                                generic_category().default_error_condition(io_errno),
×
UNCOV
524
                                "Failed to get line from deployment logs file '" + orig_path
×
UNCOV
525
                                        + "': " + strerror(io_errno));
×
526
                }
527
                if (line.empty()) {
20,086✔
528
                        // skip empty lines
529
                        continue;
23✔
530
                }
531
                auto ex_json = json::Load(line);
40,126✔
532
                if (ex_json) {
20,063✔
533
                        // valid JSON log line, just replace the newline after it with a comma and save the
534
                        // timestamp for later
535
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
40,100✔
536
                        if (ex_tstamp) {
20,050✔
537
                                if (first_tstamp.empty()) {
20,050✔
538
                                        first_tstamp = ex_tstamp.value();
22✔
539
                                }
540
                                last_known_tstamp = std::move(ex_tstamp.value());
20,050✔
541
                        }
542
                        line.append(1, ',');
20,050✔
543
                        err = io::WriteStringIntoOfstream(ofs, line);
20,050✔
544
                        if (err != error::NoError) {
20,050✔
545
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
546
                        }
547
                } else {
548
                        all_valid = false;
13✔
549
                        if (first_tstamp.empty()) {
13✔
550
                                // If we still don't have the first valid tstamp, we need to
551
                                // save the last known one (potentially pre-set) as the first
552
                                // one.
553
                                first_tstamp = last_known_tstamp;
554
                        }
555
                        err = io::WriteStringIntoOfstream(
13✔
556
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
26✔
557
                        if (err != error::NoError) {
13✔
UNCOV
558
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
559
                        }
560
                }
561
        }
562
        return error::NoError;
23✔
563
}
564

565
static void ReplaceTimestampInMsgData(
10✔
566
        vector<uint8_t> &msg_data, string &timestamp, size_t default_tstamp_size) {
567
        auto msg_data_tstamp_start = msg_data.begin() + 15; // len(R"({"timestamp": ")")
568

569
        // The actual timestamp from logs can potentially have a different
570
        // (likely lower) time resolution and thus length than our default.
571
        const auto timestamp_size = timestamp.size();
572
        if (timestamp_size > default_tstamp_size) {
10✔
573
                // In case the time resolution is higher and the timestamp
574
                // longer (unlikely to happen)
575
                if (timestamp[timestamp_size - 1] == 'Z') {
1✔
576
                        timestamp[default_tstamp_size - 1] = 'Z';
1✔
577
                }
578
                timestamp.resize(default_tstamp_size);
1✔
579
        }
580
        copy_n(timestamp.cbegin(), timestamp.size(), msg_data_tstamp_start);
10✔
581
        if (timestamp.size() < default_tstamp_size) {
10✔
582
                // Add a closing '"' right after the timestamp and fill in the
583
                // rest of the space in the template with spaces that have no
584
                // effect in JSON.
585
                msg_data_tstamp_start[timestamp.size()] = '"';
1✔
586
                for (auto it = msg_data_tstamp_start + timestamp.size() + 1;
1✔
587
                         it < msg_data_tstamp_start + default_tstamp_size + 1;
4✔
588
                         it++) {
589
                        *it = ' ';
3✔
590
                }
591
        }
592
}
10✔
593

594
error::Error JsonLogMessagesReader::SanitizeLogs() {
23✔
595
        if (!sanitized_fpath_.empty()) {
23✔
UNCOV
596
                return error::NoError;
×
597
        }
598

599
        string prep_fpath = log_fpath_ + ".sanitized";
23✔
600
        string first_tstamp = default_tstamp_;
23✔
601
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
23✔
602
        if (err != error::NoError) {
23✔
UNCOV
603
                if (path::FileExists(prep_fpath)) {
×
UNCOV
604
                        auto del_err = path::FileDelete(prep_fpath);
×
605
                        if (del_err != error::NoError) {
×
UNCOV
606
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
607
                        }
608
                }
609
        } else {
610
                sanitized_fpath_ = std::move(prep_fpath);
23✔
611
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
46✔
612
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
23✔
613
                if (!ex_sz) {
23✔
UNCOV
614
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
615
                }
616

617
                raw_data_size_ = ex_sz.value();
23✔
618
                if (raw_data_size_ > maximum_log_size_) {
23✔
619
                        large_logs_ = true;
1✔
620
                        // Make sure we end up with less data than the limit with all the
621
                        // potential extra messages added in JsonLogMessagesReader::Read()
622
                        // below.
623
                        auto ex_off = FindNextMsgAfter(
624
                                sanitized_fpath_,
625
                                (raw_data_size_ + too_much_data_msg_tmpl_.size() + bad_data_msg_tmpl_.size()
1✔
626
                                 - maximum_log_size_));
1✔
627
                        if (!ex_off) {
1✔
NEW
628
                                return ex_off.error().WithContext(
×
NEW
629
                                        "Failed to determine start offset in too large deployment logs");
×
630
                        }
631
                        reader_ = make_unique<io::FileReader>(sanitized_fpath_, ex_off.value());
2✔
632
                        raw_data_size_ -= ex_off.value();
1✔
633
                }
634
                rem_raw_data_size_ = raw_data_size_;
23✔
635
                if (!clean_logs_) {
23✔
636
                        ReplaceTimestampInMsgData(bad_data_msg_, first_tstamp, default_tstamp_.size());
9✔
637
                }
638
                if (large_logs_) {
23✔
639
                        ReplaceTimestampInMsgData(too_much_data_msg_, first_tstamp, default_tstamp_.size());
1✔
640
                }
641
        }
642
        return err;
23✔
643
}
644

645
error::Error JsonLogMessagesReader::Rewind() {
5✔
646
        AssertOrReturnError(!sanitized_fpath_.empty());
5✔
647
        header_rem_ = header_.size();
5✔
648
        closing_rem_ = closing_.size();
5✔
649
        bad_data_msg_rem_ = bad_data_msg_.size();
5✔
650
        too_much_data_msg_rem_ = too_much_data_msg_.size();
5✔
651

652
        // release/close the file first so that the FileDelete() below can actually
653
        // delete it and free space up
654
        reader_.reset();
655
        auto del_err = path::FileDelete(sanitized_fpath_);
5✔
656
        if (del_err != error::NoError) {
5✔
657
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
2✔
658
        }
659
        sanitized_fpath_.erase();
5✔
660
        return SanitizeLogs();
5✔
661
}
662

663
int64_t JsonLogMessagesReader::TotalDataSize() {
18✔
664
        assert(!sanitized_fpath_.empty());
665

666
        auto ret = raw_data_size_ + header_.size() + closing_.size();
18✔
667
        if (!clean_logs_) {
18✔
668
                ret += bad_data_msg_.size();
9✔
669
        }
670
        if (large_logs_) {
18✔
671
                ret += too_much_data_msg_.size();
1✔
672
        }
673
        return ret;
18✔
674
}
675

676
ExpectedSize JsonLogMessagesReader::Read(
1,141✔
677
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
678
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
1,141✔
679

680
        if (header_rem_ > 0) {
1,141✔
681
                io::Vsize target_size = end - start;
20✔
682
                auto copy_end = copy_n(
20✔
683
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
20✔
684
                auto n_copied = copy_end - start;
685
                header_rem_ -= n_copied;
20✔
686
                return static_cast<size_t>(n_copied);
687
        } else if (large_logs_ && (too_much_data_msg_rem_ > 0)) {
1,121✔
688
                io::Vsize target_size = end - start;
1✔
689
                auto copy_end = copy_n(
1✔
690
                        too_much_data_msg_.begin() + (too_much_data_msg_.size() - too_much_data_msg_rem_),
1✔
691
                        min(too_much_data_msg_rem_, target_size),
692
                        start);
693
                auto n_copied = copy_end - start;
694
                too_much_data_msg_rem_ -= n_copied;
1✔
695
                return static_cast<size_t>(n_copied);
696
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
1,120✔
697
                io::Vsize target_size = end - start;
16✔
698
                auto copy_end = copy_n(
16✔
699
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
16✔
700
                        min(bad_data_msg_rem_, target_size),
701
                        start);
702
                auto n_copied = copy_end - start;
703
                bad_data_msg_rem_ -= n_copied;
16✔
704
                return static_cast<size_t>(n_copied);
705
        } else if (rem_raw_data_size_ > 0) {
1,104✔
706
                if (end - start > rem_raw_data_size_) {
1,066✔
707
                        end = start + static_cast<size_t>(rem_raw_data_size_);
708
                }
709
                auto ex_sz = reader_->Read(start, end);
1,066✔
710
                if (!ex_sz) {
1,066✔
711
                        return ex_sz;
712
                }
713
                auto n_read = ex_sz.value();
1,066✔
714
                rem_raw_data_size_ -= n_read;
1,066✔
715

716
                // We control how much we read from the file so we should never read
717
                // 0 bytes (meaning EOF reached). If we do, it means the file is
718
                // smaller than what we were told.
719
                assert(n_read > 0);
720
                if (n_read == 0) {
1,066✔
UNCOV
721
                        return expected::unexpected(
×
UNCOV
722
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
723
                }
724
                return n_read;
725
        } else if (closing_rem_ > 0) {
38✔
726
                io::Vsize target_size = end - start;
19✔
727
                auto copy_end = copy_n(
19✔
728
                        closing_.begin() + (closing_.size() - closing_rem_),
19✔
729
                        min(closing_rem_, target_size),
730
                        start);
731
                auto n_copied = copy_end - start;
732
                closing_rem_ -= n_copied;
19✔
733
                return static_cast<size_t>(copy_end - start);
734
        } else {
735
                return 0;
736
        }
737
};
738

739
static const string logs_uri_suffix = "/log";
740

741
error::Error DeploymentClient::PushLogs(
4✔
742
        const string &deployment_id,
743
        const string &log_file_path,
744
        api::Client &client,
745
        LogsAPIResponseHandler api_handler) {
746
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
4✔
747
        auto err = logs_reader->SanitizeLogs();
4✔
748
        if (err != error::NoError) {
4✔
UNCOV
749
                return err;
×
750
        }
751

752
        auto req = make_shared<api::APIRequest>();
4✔
753
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
4✔
754
        req->SetMethod(http::Method::PUT);
4✔
755
        req->SetHeader("Content-Type", "application/json");
8✔
756
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
8✔
757
        req->SetHeader("Accept", "application/json");
8✔
758
        req->SetBodyGenerator([logs_reader]() {
20✔
759
                logs_reader->Rewind();
8✔
760
                return logs_reader;
4✔
761
        });
762

763
        auto received_body = make_shared<vector<uint8_t>>();
4✔
764
        return client.AsyncCall(
16✔
765
                req,
766
                [this, received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
8✔
767
                        this->PushLogsHeaderHandler(received_body, api_handler, exp_resp);
12✔
768
                },
4✔
769
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
11✔
770
                        if (!exp_resp) {
3✔
UNCOV
771
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
UNCOV
772
                                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
UNCOV
773
                                return;
×
774
                        }
775

776
                        auto resp = exp_resp.value();
3✔
777
                        auto status = resp->GetStatusCode();
3✔
778

779
                        // StatusTooManyRequests must have been handled in PushLogsHeaderHandler already
780
                        assert(status != http::StatusTooManyRequests);
781
                        // StatusRequestBodyTooLarge must have been handled in PushLogsHeaderHandler already
782
                        assert(status != http::StatusRequestBodyTooLarge);
783

784
                        if (status == http::StatusNoContent) {
3✔
785
                                api_handler(LogsAPIResponse {status, nullopt, error::NoError});
2✔
786
                        } else {
787
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
788
                                string err_str;
789
                                if (ex_err_msg) {
1✔
790
                                        err_str = ex_err_msg.value();
1✔
791
                                } else {
UNCOV
792
                                        err_str = resp->GetStatusMessage();
×
793
                                }
794
                                api_handler(LogsAPIResponse {
2✔
795
                                        status,
796
                                        nullopt,
797
                                        MakeError(
798
                                                BadResponseError,
799
                                                "Got unexpected response " + to_string(status)
1✔
800
                                                        + " from logs API: " + err_str)});
2✔
801
                        }
802
                });
4✔
803
}
804

805
void DeploymentClient::PushLogsHeaderHandler(
7✔
806
        shared_ptr<vector<uint8_t>> received_body,
807
        LogsAPIResponseHandler api_handler,
808
        http::ExpectedIncomingResponsePtr exp_resp) {
809
        if (!exp_resp) {
7✔
810
                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
UNCOV
811
                api_handler(LogsAPIResponse {nullopt, nullopt, exp_resp.error()});
×
UNCOV
812
                return;
×
813
        }
814

815
        auto body_writer = make_shared<io::ByteWriter>(received_body);
7✔
816
        auto resp = exp_resp.value();
7✔
817
        auto status = resp->GetStatusCode();
7✔
818
        if (status == http::StatusTooManyRequests) {
7✔
819
                LogsAPIResponse response = {
820
                        status, resp->GetHeaders(), MakeError(TooManyRequestsError, "Too many requests")};
3✔
821
                api_handler(response);
3✔
822
        } else if (status == http::StatusRequestBodyTooLarge) {
7✔
823
                LogsAPIResponse response = {
824
                        status,
825
                        resp->GetHeaders(),
826
                        MakeError(RequestBodyTooLargeError, "Could not send logs to server")};
1✔
827
                api_handler(response);
1✔
828
        }
1✔
829
        auto content_length = resp->GetHeader("Content-Length");
14✔
830
        if (!content_length) {
7✔
831
                log::Debug(
3✔
832
                        "Failed to get content length from the deployment log API response headers: "
833
                        + content_length.error().String());
6✔
834
                body_writer->SetUnlimited(true);
3✔
835
        } else {
836
                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
837
                if (!ex_len) {
4✔
UNCOV
838
                        log::Error(
×
839
                                "Failed to convert the content length from the deployment log API response headers to an integer: "
UNCOV
840
                                + ex_len.error().String());
×
UNCOV
841
                        body_writer->SetUnlimited(true);
×
842
                } else {
843
                        received_body->resize(ex_len.value());
4✔
844
                }
845
        }
846
        resp->SetBodyWriter(body_writer);
14✔
847
}
848

849
} // namespace deployments
850
} // namespace update
851
} // namespace mender
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc