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

mendersoftware / mender / 2302561441

02 Feb 2026 04:12PM UTC coverage: 75.803%. First build
2302561441

push

gitlab-ci

vpodzime
fix: Make JSONL parsing to deployment log more robust

Changelog: Sanitize deployments logs from disk before sending them to
the server. Fixes the issue that a corrupted log file will trigger HTTP
400 Bad Request from the server and the logs would never be submitted.
With this fix the corrupted line(s) are replaced with a known error
entry "(corrupted log)", savaging the rest of the well-formatted logs.

Ticket: MEN-9128

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Lluis Campos <lluis.campos@northern.tech>
Signed-off-by: Vratislav Podzimek <vratislav.podzimek+auto-signed@northern.tech>
(cherry picked from commit 39f4a3d66)

81 of 98 new or added lines in 1 file covered. (82.65%)

7506 of 9902 relevant lines covered (75.8%)

13706.27 hits per line

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

79.01
/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
const DeploymentsErrorCategoryClass DeploymentsErrorCategory;
54

55
const char *DeploymentsErrorCategoryClass::name() const noexcept {
×
56
        return "DeploymentsErrorCategory";
×
57
}
58

59
string DeploymentsErrorCategoryClass::message(int code) const {
3✔
60
        switch (code) {
3✔
61
        case NoError:
62
                return "Success";
×
63
        case InvalidDataError:
64
                return "Invalid data error";
×
65
        case BadResponseError:
66
                return "Bad response error";
×
67
        case DeploymentAbortedError:
68
                return "Deployment was aborted on the server";
3✔
69
        }
70
        assert(false);
71
        return "Unknown";
×
72
}
73

74
error::Error MakeError(DeploymentsErrorCode code, const string &msg) {
30✔
75
        return error::Error(error_condition(code, DeploymentsErrorCategory), msg);
35✔
76
}
77

78
static const string check_updates_v1_uri = "/api/devices/v1/deployments/device/deployments/next";
79
static const string check_updates_v2_uri = "/api/devices/v2/deployments/device/deployments/next";
80

81
error::Error DeploymentClient::CheckNewDeployments(
7✔
82
        context::MenderContext &ctx, api::Client &client, CheckUpdatesAPIResponseHandler api_handler) {
83
        auto ex_dev_type = ctx.GetDeviceType();
7✔
84
        if (!ex_dev_type) {
7✔
85
                return ex_dev_type.error();
1✔
86
        }
87
        string device_type = ex_dev_type.value();
6✔
88

89
        auto ex_provides = ctx.LoadProvides();
6✔
90
        if (!ex_provides) {
6✔
91
                return ex_provides.error();
×
92
        }
93
        auto provides = ex_provides.value();
6✔
94
        if (provides.find("artifact_name") == provides.end()) {
12✔
95
                return MakeError(InvalidDataError, "Missing artifact name data");
×
96
        }
97

98
        stringstream ss;
12✔
99
        ss << R"({"device_provides":{)";
6✔
100
        ss << R"("device_type":")";
6✔
101
        ss << json::EscapeString(device_type);
12✔
102

103
        for (const auto &kv : provides) {
14✔
104
                ss << "\",\"" + json::EscapeString(kv.first) + "\":\"";
16✔
105
                ss << json::EscapeString(kv.second);
16✔
106
        }
107

108
        ss << R"("}})";
6✔
109

110
        string v2_payload = ss.str();
111
        log::Debug("deployments/next v2 payload " + v2_payload);
6✔
112
        http::BodyGenerator payload_gen = [v2_payload]() {
48✔
113
                return make_shared<io::StringReader>(v2_payload);
6✔
114
        };
6✔
115

116
        auto v2_req = make_shared<api::APIRequest>();
6✔
117
        v2_req->SetPath(check_updates_v2_uri);
118
        v2_req->SetMethod(http::Method::POST);
6✔
119
        v2_req->SetHeader("Content-Type", "application/json");
12✔
120
        v2_req->SetHeader("Content-Length", to_string(v2_payload.size()));
12✔
121
        v2_req->SetHeader("Accept", "application/json");
12✔
122
        v2_req->SetBodyGenerator(payload_gen);
6✔
123

124
        string v1_args = "artifact_name=" + http::URLEncode(provides["artifact_name"])
18✔
125
                                         + "&device_type=" + http::URLEncode(device_type);
18✔
126
        auto v1_req = make_shared<api::APIRequest>();
6✔
127
        v1_req->SetPath(check_updates_v1_uri + "?" + v1_args);
12✔
128
        v1_req->SetMethod(http::Method::GET);
6✔
129
        v1_req->SetHeader("Accept", "application/json");
12✔
130

131
        auto received_body = make_shared<vector<uint8_t>>();
6✔
132
        auto handle_data = [received_body, api_handler](unsigned status) {
4✔
133
                if (status == http::StatusOK) {
4✔
134
                        auto ex_j = json::Load(common::StringFromByteVector(*received_body));
4✔
135
                        if (ex_j) {
2✔
136
                                CheckUpdatesAPIResponse response {optional<json::Json> {ex_j.value()}};
2✔
137
                                api_handler(response);
4✔
138
                        } else {
139
                                api_handler(expected::unexpected(ex_j.error()));
×
140
                        }
141
                } else if (status == http::StatusNoContent) {
2✔
142
                        api_handler(CheckUpdatesAPIResponse {nullopt});
4✔
143
                } else {
144
                        log::Warning(
×
145
                                "DeploymentClient::CheckNewDeployments - received unhandled http response: "
146
                                + to_string(status));
×
147
                        api_handler(expected::unexpected(MakeError(
×
148
                                DeploymentAbortedError, "received unhandled HTTP response: " + to_string(status))));
×
149
                }
150
        };
16✔
151

152
        http::ResponseHandler header_handler =
153
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
9✔
154
                        if (!exp_resp) {
9✔
155
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
156
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
157
                                api_handler(response);
×
158
                                return;
159
                        }
160

161
                        auto resp = exp_resp.value();
9✔
162
                        received_body->clear();
9✔
163
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
9✔
164
                        body_writer->SetUnlimited(true);
9✔
165
                        resp->SetBodyWriter(body_writer);
18✔
166
                };
12✔
167

168
        http::ResponseHandler v1_body_handler =
169
                [received_body, api_handler, handle_data](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
170
                        if (!exp_resp) {
3✔
171
                                log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
172
                                CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
173
                                api_handler(response);
×
174
                                return;
175
                        }
176
                        auto resp = exp_resp.value();
3✔
177
                        auto status = resp->GetStatusCode();
3✔
178
                        if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
3✔
179
                                handle_data(status);
2✔
180
                        } else {
181
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
182
                                string err_str;
183
                                if (ex_err_msg) {
1✔
184
                                        err_str = ex_err_msg.value();
×
185
                                } else {
186
                                        err_str = resp->GetStatusMessage();
2✔
187
                                }
188
                                api_handler(expected::unexpected(MakeError(
2✔
189
                                        BadResponseError,
190
                                        "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
191
                        }
192
                };
12✔
193

194
        http::ResponseHandler v2_body_handler = [received_body,
6✔
195
                                                                                         v1_req,
196
                                                                                         header_handler,
197
                                                                                         v1_body_handler,
198
                                                                                         api_handler,
199
                                                                                         handle_data,
200
                                                                                         &client](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
201
                if (!exp_resp) {
6✔
202
                        log::Error("Request to check new deployments failed: " + exp_resp.error().message);
×
203
                        CheckUpdatesAPIResponse response = expected::unexpected(exp_resp.error());
×
204
                        api_handler(response);
×
205
                        return;
206
                }
207
                auto resp = exp_resp.value();
6✔
208
                auto status = resp->GetStatusCode();
6✔
209
                if ((status == http::StatusOK) || (status == http::StatusNoContent)) {
6✔
210
                        handle_data(status);
2✔
211
                } else if (status == http::StatusNotFound) {
4✔
212
                        log::Debug(
3✔
213
                                "POST request to v2 version of the deployments API failed, falling back to v1 version and GET");
6✔
214
                        auto err = client.AsyncCall(v1_req, header_handler, v1_body_handler);
9✔
215
                        if (err != error::NoError) {
3✔
216
                                api_handler(expected::unexpected(err.WithContext("While calling v1 endpoint")));
×
217
                        }
218
                } else {
219
                        auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
220
                        string err_str;
221
                        if (ex_err_msg) {
1✔
222
                                err_str = ex_err_msg.value();
1✔
223
                        } else {
224
                                err_str = resp->GetStatusMessage();
×
225
                        }
226
                        api_handler(expected::unexpected(MakeError(
2✔
227
                                BadResponseError,
228
                                "Got unexpected response " + to_string(status) + ": " + err_str)));
4✔
229
                }
230
        };
6✔
231

232
        return client.AsyncCall(v2_req, header_handler, v2_body_handler);
18✔
233
}
234

235
static const string deployment_status_strings[static_cast<int>(DeploymentStatus::End_) + 1] = {
236
        "installing",
237
        "pause_before_installing",
238
        "downloading",
239
        "pause_before_rebooting",
240
        "rebooting",
241
        "pause_before_committing",
242
        "success",
243
        "failure",
244
        "already-installed"};
245

246
static const string deployments_uri_prefix = "/api/devices/v1/deployments/device/deployments";
247
static const string status_uri_suffix = "/status";
248

249
string DeploymentStatusString(DeploymentStatus status) {
501✔
250
        return deployment_status_strings[static_cast<int>(status)];
505✔
251
}
252

253
error::Error DeploymentClient::PushStatus(
4✔
254
        const string &deployment_id,
255
        DeploymentStatus status,
256
        const string &substate,
257
        api::Client &client,
258
        StatusAPIResponseHandler api_handler) {
259
        // Cannot push a status update without a deployment ID
260
        AssertOrReturnError(deployment_id != "");
4✔
261
        string payload = R"({"status":")" + DeploymentStatusString(status) + "\"";
8✔
262
        if (substate != "") {
4✔
263
                payload += R"(,"substate":")" + json::EscapeString(substate) + "\"}";
6✔
264
        } else {
265
                payload += "}";
1✔
266
        }
267
        http::BodyGenerator payload_gen = [payload]() {
32✔
268
                return make_shared<io::StringReader>(payload);
4✔
269
        };
4✔
270

271
        auto req = make_shared<api::APIRequest>();
4✔
272
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, status_uri_suffix));
8✔
273
        req->SetMethod(http::Method::PUT);
4✔
274
        req->SetHeader("Content-Type", "application/json");
8✔
275
        req->SetHeader("Content-Length", to_string(payload.size()));
8✔
276
        req->SetHeader("Accept", "application/json");
8✔
277
        req->SetBodyGenerator(payload_gen);
4✔
278

279
        auto received_body = make_shared<vector<uint8_t>>();
4✔
280
        return client.AsyncCall(
281
                req,
282
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
283
                        if (!exp_resp) {
4✔
284
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
285
                                api_handler(exp_resp.error());
×
286
                                return;
×
287
                        }
288

289
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
4✔
290
                        auto resp = exp_resp.value();
4✔
291
                        auto content_length = resp->GetHeader("Content-Length");
8✔
292
                        if (!content_length) {
4✔
293
                                log::Debug(
×
294
                                        "Failed to get content length from the deployment status API response headers: "
295
                                        + content_length.error().String());
×
296
                                body_writer->SetUnlimited(true);
×
297
                        } else {
298
                                auto ex_len = common::StringTo<size_t>(content_length.value());
4✔
299
                                if (!ex_len) {
4✔
300
                                        log::Error(
×
301
                                                "Failed to convert the content length from the deployment status API response headers to an integer: "
302
                                                + ex_len.error().String());
×
303
                                        body_writer->SetUnlimited(true);
×
304
                                } else {
305
                                        received_body->resize(ex_len.value());
4✔
306
                                }
307
                        }
308
                        resp->SetBodyWriter(body_writer);
8✔
309
                },
310
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
4✔
311
                        if (!exp_resp) {
4✔
312
                                log::Error("Request to push status data failed: " + exp_resp.error().message);
×
313
                                api_handler(exp_resp.error());
×
314
                                return;
×
315
                        }
316

317
                        auto resp = exp_resp.value();
4✔
318
                        auto status = resp->GetStatusCode();
4✔
319
                        if (status == http::StatusNoContent) {
4✔
320
                                api_handler(error::NoError);
4✔
321
                        } else if (status == http::StatusConflict) {
2✔
322
                                api_handler(
1✔
323
                                        MakeError(DeploymentAbortedError, "Could not send status update to server"));
2✔
324
                        } else {
325
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
326
                                string err_str;
327
                                if (ex_err_msg) {
1✔
328
                                        err_str = ex_err_msg.value();
1✔
329
                                } else {
330
                                        err_str = resp->GetStatusMessage();
×
331
                                }
332
                                api_handler(MakeError(
2✔
333
                                        BadResponseError,
334
                                        "Got unexpected response " + to_string(status)
2✔
335
                                                + " from status API: " + err_str));
2✔
336
                        }
337
                });
16✔
338
}
339

340
using mender::common::expected::ExpectedSize;
341

342
static ExpectedSize GetLogFileDataSize(const string &path) {
18✔
343
        auto ex_istr = io::OpenIfstream(path);
18✔
344
        if (!ex_istr) {
18✔
345
                return expected::unexpected(ex_istr.error());
×
346
        }
347
        auto istr = std::move(ex_istr.value());
36✔
348

349
        // We want the size of the actual data without a potential trailing
350
        // comma. So let's seek one byte before the end of file, check if the last
351
        // byte is a comma and return the appropriate number.
352
        istr.seekg(-1, ios_base::end);
18✔
353
        int c = istr.get();
18✔
354
        if (c == ',') {
18✔
355
                return istr.tellg() - static_cast<ifstream::off_type>(1);
18✔
356
        } else {
357
                return istr.tellg();
×
358
        }
359
}
360

361
const vector<uint8_t> JsonLogMessagesReader::header_ = {
362
        '{', '"', 'm', 'e', 's', 's', 'a', 'g', 'e', 's', '"', ':', '['};
363
const vector<uint8_t> JsonLogMessagesReader::closing_ = {']', '}'};
364
const string JsonLogMessagesReader::default_tstamp_ = "1970-01-01T00:00:00.000000000Z";
365
const string JsonLogMessagesReader::bad_data_msg_tmpl_ =
366
        R"d({"timestamp": "1970-01-01T00:00:00.000000000Z", "level": "ERROR", "message": "(THE ORIGINAL LOGS CONTAINED INVALID ENTRIES)"},)d";
367

368
JsonLogMessagesReader::~JsonLogMessagesReader() {
42✔
369
        reader_.reset();
370
        if (!sanitized_fpath_.empty() && path::FileExists(sanitized_fpath_)) {
14✔
371
                auto del_err = path::FileDelete(sanitized_fpath_);
14✔
372
                if (del_err != error::NoError) {
14✔
NEW
373
                        log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
374
                }
375
        }
376
        sanitized_fpath_.erase();
14✔
377
}
14✔
378

379
static error::Error DoSanitizeLogs(
18✔
380
        const string &orig_path, const string &new_path, bool &all_valid, string &first_tstamp) {
381
        auto ex_ifs = io::OpenIfstream(orig_path);
18✔
382
        if (!ex_ifs) {
18✔
NEW
383
                return ex_ifs.error();
×
384
        }
385
        auto ex_ofs = io::OpenOfstream(new_path);
18✔
386
        if (!ex_ofs) {
18✔
NEW
387
                return ex_ofs.error();
×
388
        }
389
        auto &ifs = ex_ifs.value();
18✔
390
        auto &ofs = ex_ofs.value();
18✔
391

392
        string last_known_tstamp = first_tstamp;
18✔
393
        const string tstamp_prefix_data = R"d({"timestamp": ")d";
18✔
394
        const string corrupt_msg_suffix_data =
395
                R"d(", "level": "ERROR", "message": "(CORRUPTED LOG DATA)"},)d";
18✔
396

397
        string line;
398
        first_tstamp.erase();
18✔
399
        all_valid = true;
18✔
400
        error::Error err;
18✔
401
        while (!ifs.eof()) {
86✔
402
                getline(ifs, line);
68✔
403
                if (!ifs.eof() && !ifs) {
68✔
NEW
404
                        int io_errno = errno;
×
405
                        return error::Error(
NEW
406
                                generic_category().default_error_condition(io_errno),
×
NEW
407
                                "Failed to get line from deployment logs file '" + orig_path
×
NEW
408
                                        + "': " + strerror(io_errno));
×
409
                }
410
                if (line.empty()) {
68✔
411
                        // skip empty lines
412
                        continue;
18✔
413
                }
414
                auto ex_json = json::Load(line);
100✔
415
                if (ex_json) {
50✔
416
                        // valid JSON log line, just replace the newline after it with a comma and save the
417
                        // timestamp for later
418
                        auto ex_tstamp = ex_json.value().Get("timestamp").and_then(json::ToString);
82✔
419
                        if (ex_tstamp) {
41✔
420
                                if (first_tstamp.empty()) {
41✔
421
                                        first_tstamp = ex_tstamp.value();
17✔
422
                                }
423
                                last_known_tstamp = std::move(ex_tstamp.value());
41✔
424
                        }
425
                        line.append(1, ',');
41✔
426
                        err = io::WriteStringIntoOfstream(ofs, line);
41✔
427
                        if (err != error::NoError) {
41✔
NEW
428
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
429
                        }
430
                } else {
431
                        all_valid = false;
9✔
432
                        if (first_tstamp.empty()) {
9✔
433
                                // If we still don't have the first valid tstamp, we need to
434
                                // save the last known one (potentially pre-set) as the first
435
                                // one.
436
                                first_tstamp = last_known_tstamp;
437
                        }
438
                        err = io::WriteStringIntoOfstream(
9✔
439
                                ofs, tstamp_prefix_data + last_known_tstamp + corrupt_msg_suffix_data);
18✔
440
                        if (err != error::NoError) {
9✔
NEW
441
                                return err.WithContext("Failed to write pre-processed deployment logs data");
×
442
                        }
443
                }
444
        }
445
        return error::NoError;
18✔
446
}
447

448
error::Error JsonLogMessagesReader::SanitizeLogs() {
18✔
449
        if (!sanitized_fpath_.empty()) {
18✔
NEW
450
                return error::NoError;
×
451
        }
452

453
        string prep_fpath = log_fpath_ + ".sanitized";
18✔
454
        string first_tstamp = default_tstamp_;
18✔
455
        auto err = DoSanitizeLogs(log_fpath_, prep_fpath, clean_logs_, first_tstamp);
18✔
456
        if (err != error::NoError) {
18✔
NEW
457
                if (path::FileExists(prep_fpath)) {
×
NEW
458
                        auto del_err = path::FileDelete(prep_fpath);
×
NEW
459
                        if (del_err != error::NoError) {
×
NEW
460
                                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
461
                        }
462
                }
463
        } else {
464
                sanitized_fpath_ = std::move(prep_fpath);
18✔
465
                reader_ = make_unique<io::FileReader>(sanitized_fpath_);
36✔
466
                auto ex_sz = GetLogFileDataSize(sanitized_fpath_);
18✔
467
                if (!ex_sz) {
18✔
NEW
468
                        return ex_sz.error().WithContext("Failed to determine deployment logs size");
×
469
                }
470
                raw_data_size_ = ex_sz.value();
18✔
471
                rem_raw_data_size_ = raw_data_size_;
18✔
472
                if (!clean_logs_) {
18✔
473
                        auto bad_data_msg_tstamp_start =
474
                                bad_data_msg_.begin() + 15; // len(R"({"timestamp": ")")
475
                        copy_n(first_tstamp.cbegin(), first_tstamp.size(), bad_data_msg_tstamp_start);
7✔
476
                }
477
        }
478
        return err;
18✔
479
}
480

481
error::Error JsonLogMessagesReader::Rewind() {
4✔
482
        AssertOrReturnError(!sanitized_fpath_.empty());
4✔
483
        header_rem_ = header_.size();
4✔
484
        closing_rem_ = closing_.size();
4✔
485
        bad_data_msg_rem_ = bad_data_msg_.size();
4✔
486

487
        // release/close the file first so that the FileDelete() below can actually
488
        // delete it and free space up
489
        reader_.reset();
490
        auto del_err = path::FileDelete(sanitized_fpath_);
4✔
491
        if (del_err != error::NoError) {
4✔
NEW
492
                log::Error("Failed to delete auxiliary logs file: " + del_err.String());
×
493
        }
494
        sanitized_fpath_.erase();
4✔
495
        return SanitizeLogs();
4✔
496
}
497

498
int64_t JsonLogMessagesReader::TotalDataSize() {
14✔
499
        assert(!sanitized_fpath_.empty());
500

501
        auto ret = raw_data_size_ + header_.size() + closing_.size();
14✔
502
        if (!clean_logs_) {
14✔
503
                ret += bad_data_msg_.size();
7✔
504
        }
505
        return ret;
14✔
506
}
507

508
ExpectedSize JsonLogMessagesReader::Read(
147✔
509
        vector<uint8_t>::iterator start, vector<uint8_t>::iterator end) {
510
        AssertOrReturnUnexpected(!sanitized_fpath_.empty());
147✔
511

512
        if (header_rem_ > 0) {
147✔
513
                io::Vsize target_size = end - start;
16✔
514
                auto copy_end = copy_n(
515
                        header_.begin() + (header_.size() - header_rem_), min(header_rem_, target_size), start);
17✔
516
                auto n_copied = copy_end - start;
517
                header_rem_ -= n_copied;
16✔
518
                return static_cast<size_t>(n_copied);
519
        } else if (!clean_logs_ && (bad_data_msg_rem_ > 0)) {
131✔
520
                io::Vsize target_size = end - start;
14✔
521
                auto copy_end = copy_n(
522
                        bad_data_msg_.begin() + (bad_data_msg_.size() - bad_data_msg_rem_),
14✔
523
                        min(bad_data_msg_rem_, target_size),
14✔
524
                        start);
14✔
525
                auto n_copied = copy_end - start;
526
                bad_data_msg_rem_ -= n_copied;
14✔
527
                return static_cast<size_t>(n_copied);
528
        } else if (rem_raw_data_size_ > 0) {
117✔
529
                if (end - start > rem_raw_data_size_) {
87✔
530
                        end = start + static_cast<size_t>(rem_raw_data_size_);
531
                }
532
                auto ex_sz = reader_->Read(start, end);
87✔
533
                if (!ex_sz) {
87✔
534
                        return ex_sz;
535
                }
536
                auto n_read = ex_sz.value();
87✔
537
                rem_raw_data_size_ -= n_read;
87✔
538

539
                // We control how much we read from the file so we should never read
540
                // 0 bytes (meaning EOF reached). If we do, it means the file is
541
                // smaller than what we were told.
542
                assert(n_read > 0);
543
                if (n_read == 0) {
87✔
544
                        return expected::unexpected(
×
545
                                MakeError(InvalidDataError, "Unexpected EOF when reading logs file"));
×
546
                }
547
                return n_read;
548
        } else if (closing_rem_ > 0) {
30✔
549
                io::Vsize target_size = end - start;
15✔
550
                auto copy_end = copy_n(
551
                        closing_.begin() + (closing_.size() - closing_rem_),
15✔
552
                        min(closing_rem_, target_size),
15✔
553
                        start);
15✔
554
                auto n_copied = copy_end - start;
555
                closing_rem_ -= n_copied;
15✔
556
                return static_cast<size_t>(copy_end - start);
557
        } else {
558
                return 0;
559
        }
560
};
561

562
static const string logs_uri_suffix = "/log";
563

564
error::Error DeploymentClient::PushLogs(
3✔
565
        const string &deployment_id,
566
        const string &log_file_path,
567
        api::Client &client,
568
        LogsAPIResponseHandler api_handler) {
569
        auto logs_reader = make_shared<JsonLogMessagesReader>(log_file_path);
3✔
570
        auto err = logs_reader->SanitizeLogs();
3✔
571
        if (err != error::NoError) {
3✔
NEW
572
                return err;
×
573
        }
574

575
        auto req = make_shared<api::APIRequest>();
3✔
576
        req->SetPath(http::JoinUrl(deployments_uri_prefix, deployment_id, logs_uri_suffix));
6✔
577
        req->SetMethod(http::Method::PUT);
3✔
578
        req->SetHeader("Content-Type", "application/json");
6✔
579
        req->SetHeader("Content-Length", to_string(logs_reader->TotalDataSize()));
6✔
580
        req->SetHeader("Accept", "application/json");
6✔
581
        req->SetBodyGenerator([logs_reader]() {
18✔
582
                logs_reader->Rewind();
6✔
583
                return logs_reader;
3✔
584
        });
6✔
585

586
        auto received_body = make_shared<vector<uint8_t>>();
3✔
587
        return client.AsyncCall(
588
                req,
589
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
590
                        if (!exp_resp) {
3✔
591
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
592
                                api_handler(exp_resp.error());
×
593
                                return;
×
594
                        }
595

596
                        auto body_writer = make_shared<io::ByteWriter>(received_body);
3✔
597
                        auto resp = exp_resp.value();
3✔
598
                        auto content_length = resp->GetHeader("Content-Length");
6✔
599
                        if (!content_length) {
3✔
600
                                log::Debug(
×
601
                                        "Failed to get content length from the deployment log API response headers: "
602
                                        + content_length.error().String());
×
603
                                body_writer->SetUnlimited(true);
×
604
                        } else {
605
                                auto ex_len = common::StringTo<size_t>(content_length.value());
3✔
606
                                if (!ex_len) {
3✔
607
                                        log::Error(
×
608
                                                "Failed to convert the content length from the deployment log API response headers to an integer: "
609
                                                + ex_len.error().String());
×
610
                                        body_writer->SetUnlimited(true);
×
611
                                } else {
612
                                        received_body->resize(ex_len.value());
3✔
613
                                }
614
                        }
615
                        resp->SetBodyWriter(body_writer);
6✔
616
                },
617
                [received_body, api_handler](http::ExpectedIncomingResponsePtr exp_resp) {
3✔
618
                        if (!exp_resp) {
3✔
619
                                log::Error("Request to push logs data failed: " + exp_resp.error().message);
×
620
                                api_handler(exp_resp.error());
×
621
                                return;
×
622
                        }
623

624
                        auto resp = exp_resp.value();
3✔
625
                        auto status = resp->GetStatusCode();
3✔
626
                        if (status == http::StatusNoContent) {
3✔
627
                                api_handler(error::NoError);
4✔
628
                        } else {
629
                                auto ex_err_msg = api::ErrorMsgFromErrorResponse(*received_body);
1✔
630
                                string err_str;
631
                                if (ex_err_msg) {
1✔
632
                                        err_str = ex_err_msg.value();
1✔
633
                                } else {
634
                                        err_str = resp->GetStatusMessage();
×
635
                                }
636
                                api_handler(MakeError(
2✔
637
                                        BadResponseError,
638
                                        "Got unexpected response " + to_string(status) + " from logs API: " + err_str));
2✔
639
                        }
640
                });
12✔
641
}
642

643
} // namespace deployments
644
} // namespace update
645
} // 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